From b02629b6ec6f00137c3dd22898c40e5c6ec8a397 Mon Sep 17 00:00:00 2001 From: arikorn Date: Fri, 17 Oct 2025 20:34:33 -0700 Subject: [PATCH 01/12] Steamdeck Plus LCD strip swipe actions - Horizontal swipe changes pages - Vertical swipe sends rotate actions - (Added a swipe action to `SurfaceHandler` to handle the page change.) --- companion/lib/Surface/Handler.ts | 20 ++++++++++ companion/lib/Surface/Types.ts | 1 + companion/lib/Surface/USB/ElgatoStreamDeck.ts | 37 ++++++++++++++++--- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/companion/lib/Surface/Handler.ts b/companion/lib/Surface/Handler.ts index 8e3a7af839..996e54fec3 100644 --- a/companion/lib/Surface/Handler.ts +++ b/companion/lib/Surface/Handler.ts @@ -259,6 +259,7 @@ export class SurfaceHandler extends EventEmitter { this.panel.on('click', this.#onDeviceClick.bind(this)) this.panel.on('rotate', this.#onDeviceRotate.bind(this)) + this.panel.on('swipePage', this.#onDeviceSwipePage.bind(this)) this.panel.on('pincodeKey', this.#onDevicePincodeKey.bind(this)) this.panel.on('remove', this.#onDeviceRemove.bind(this)) this.panel.on('resized', this.#onDeviceResized.bind(this)) @@ -582,6 +583,25 @@ export class SurfaceHandler extends EventEmitter { } } + #onDeviceSwipePage(forward: boolean): void { + if (this.#isSurfaceLocked || !this.panel) return + + const pageNumber = this.#pageStore.getPageNumber(this.#currentPageId) + if (!pageNumber) return + + if (this.#surfaces.deviceAllowsSwipeToChangePage(this.surfaceId, true)) { + try { + this.#surfaces.devicePageSet(this.surfaceId, forward ? '+1' : '-1', true) + this.#logger.debug(`Swipe page ${pageNumber}: ${forward ? 'forward' : 'backward'}`) + } catch (e) { + this.#logger.error(`Swipe failed: ${e}`) + } + } else { + const name = this.#surfaces.getGroupNameFromDeviceId(this.surfaceId, true) + this.#logger.debug(`Swipe page ${pageNumber} disabled in Surface/Group config for "${name}".`) + } + } + #onDeviceRotate(x: number, y: number, rightward: boolean, pageOffset?: number): void { if (!this.panel) return diff --git a/companion/lib/Surface/Types.ts b/companion/lib/Surface/Types.ts index eb33fe476f..52ba0f2e31 100644 --- a/companion/lib/Surface/Types.ts +++ b/companion/lib/Surface/Types.ts @@ -73,6 +73,7 @@ export interface SurfacePanelEvents { click: [x: number, y: number, pressed: boolean, pageOffset?: number] rotate: [x: number, y: number, rightward: boolean, pageOffset?: number] + swipePage: [forward: boolean] pincodeKey: [key: number] setCustomVariable: [variableId: string, value: CompanionVariableValue] diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index 8c0479ad9e..b72b01a126 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -98,6 +98,10 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter readonly info: SurfacePanelInfo readonly gridSize: GridSize + readonly sdPlusLcdButtonOffset = 25 + readonly sdPlusLcdButtonSpacing = 216.666 + // readonly sdPlusLcdButtonWidth = 100 // not currently used, but could, if we wanted to be more precise about button locations + constructor(devicePath: string, streamDeck: StreamDeck | StreamDeckTcp) { super() @@ -203,7 +207,7 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter let drawX = drawColumn * columnWidth if (this.#streamDeck.MODEL === DeviceModelId.PLUS) { // Position aligned with the buttons/encoders - drawX = drawColumn * 216.666 + 25 + drawX = drawColumn * this.sdPlusLcdButtonSpacing + this.sdPlusLcdButtonOffset } const targetSize = control.pixelSize.height @@ -278,17 +282,40 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter this.emit('setCustomVariable', variableId, tag) }) + const getLCDButton = (control: StreamDeckLcdSegmentControlDefinition, x: number) => { + // Button assignment is very permissive, but maybe more compatible with the graphics overhaul? + // note: this doesn't take into account the SD Plus button offset, but that gives a little margin to the left, so maybe OK. + // TODO: reexamine when double-width buttons are implemented? + // if using the margin, add Math.max(0, Math.min(control.columnSpan-1, ... )) + const columnOffset = Math.floor((x / control.pixelSize.width) * control.columnSpan) + return control.column + columnOffset + } const lcdPress = (control: StreamDeckLcdSegmentControlDefinition, position: LcdPosition) => { - const columnOffset = Math.floor((position.x / control.pixelSize.width) * control.columnSpan) - - this.emit('click', control.column + columnOffset, control.row, true) + const buttonCol = getLCDButton(control, position.x) + this.emit('click', buttonCol, control.row, true) setTimeout(() => { - this.emit('click', control.column + columnOffset, control.row, false) + this.emit('click', buttonCol, control.row, false) }, 20) } this.#streamDeck.on('lcdShortPress', lcdPress) this.#streamDeck.on('lcdLongPress', lcdPress) + this.#streamDeck.on('lcdSwipe', (control, from, to) => { + const angle = Math.atan(Math.abs((from.y - to.y) / (from.x - to.x))) * (180 / Math.PI) + const fromButton = getLCDButton(control, from.x) + const toButton = getLCDButton(control, to.x) + this.#logger.debug( + `LCD #${control.id} swipe: (${from.x}, ${from.y}; button:${fromButton})->(${to.x}, ${to.y}; button: ${toButton}): Angle: ${angle.toFixed(1)}` + ) + // avoid ambiguous swipes, so vertical has to be "clearly vertical", so make it a bit more than 45 + if (angle >= 50 && toButton === fromButton) { + //vertical swipe. note that y=0 is the top of the screen so for swipe up `from.y` is the higher + this.emit('rotate', fromButton, control.row, from.y > to.y) + } else if (angle <= 22.5) { + // horizontal swipe, change pages: (note that the angle of the SD+ screen diagonal is 7 degrees, i.e. atan 1/8) + this.emit('swipePage', from.x > to.x) //swipe left moves to next page, as if your finger is moving a piece of paper under the screen + } + }) } async #init() { From 1978d2a68f9c2158dd084ab7506e67804ad5b124 Mon Sep 17 00:00:00 2001 From: arikorn Date: Sat, 18 Oct 2025 00:32:41 -0700 Subject: [PATCH 02/12] Add options to disable page-swipe and to restrict page access - Options are added to `SurfaceGroupConfig` - Modify *EditPanel.tsx* to add the new options - Modify `SurfaceGroup.setCurrentPage` to handle page restrictions - Import/Export new fields (pages are converted similarly to `startup_page_id` and `last_page_id` - (Modify `InternalPageIdDropdown` to allow multiple values) --- companion/lib/ImportExport/Controller.ts | 5 ++ companion/lib/ImportExport/Export.ts | 3 ++ companion/lib/Surface/Controller.ts | 20 ++++++++ companion/lib/Surface/Group.ts | 30 +++++++++++- shared-lib/lib/Model/Surfaces.ts | 9 +++- webui/src/Controls/InternalModuleField.tsx | 8 +++- webui/src/Surfaces/EditPanel.tsx | 56 +++++++++++++++++++++- 7 files changed, 124 insertions(+), 7 deletions(-) diff --git a/companion/lib/ImportExport/Controller.ts b/companion/lib/ImportExport/Controller.ts index 1de5f528ae..f09a3f6fd3 100644 --- a/companion/lib/ImportExport/Controller.ts +++ b/companion/lib/ImportExport/Controller.ts @@ -571,6 +571,11 @@ export class ImportExportController { groupConfig.startup_page_id = getPageId(groupConfig.startup_page!) delete groupConfig.startup_page } + if (groupConfig.allowed_pages !== undefined) { + const page_ids = groupConfig.allowed_pages.map((nr) => getPageId(nr)) + groupConfig.allowed_page_ids = page_ids + delete groupConfig.allowed_pages + } } // Convert external page refs, i.e. page numbers, to internal ids. diff --git a/companion/lib/ImportExport/Export.ts b/companion/lib/ImportExport/Export.ts index 4b933cbcdb..56e827cbab 100644 --- a/companion/lib/ImportExport/Export.ts +++ b/companion/lib/ImportExport/Export.ts @@ -570,6 +570,9 @@ export class ExportController { groupConfig.last_page = findPage(groupConfig.last_page_id) ?? 1 groupConfig.startup_page = findPage(groupConfig.startup_page_id) ?? 1 + if (groupConfig.allowed_page_ids !== undefined) { + groupConfig.allowed_pages = groupConfig.allowed_page_ids.map((id) => findPage(id) ?? 1) + } } for (const surface of Object.values(surfaces)) { diff --git a/companion/lib/Surface/Controller.ts b/companion/lib/Surface/Controller.ts index d171633937..c38641cba3 100644 --- a/companion/lib/Surface/Controller.ts +++ b/companion/lib/Surface/Controller.ts @@ -1571,6 +1571,17 @@ export class SurfaceController extends EventEmitter { return undefined } } + /** + * Get permission for swipe to change pages + */ + deviceAllowsSwipeToChangePage(surfaceOrGroupId: string, looseIdMatching = false): boolean | undefined { + const surfaceGroup = this.#getGroupForId(surfaceOrGroupId, looseIdMatching) + if (surfaceGroup) { + return surfaceGroup.groupConfig.swipe_can_change_page + } else { + return undefined + } + } /** * Get the groupId for a surfaceId (or groupId) @@ -1581,6 +1592,15 @@ export class SurfaceController extends EventEmitter { return surfaceGroup?.groupId } + /** + * Get the groupId for a surfaceId (or groupId) + */ + getGroupNameFromDeviceId(surfaceOrGroupId: string, looseIdMatching = false): string | undefined { + const surfaceGroup = this.#getGroupForId(surfaceOrGroupId, looseIdMatching) + + return surfaceGroup?.displayName + } + #resetAllDevices() { // Destroy any groups and detach their contents for (const surfaceGroup of this.#surfaceGroups.values()) { diff --git a/companion/lib/Surface/Group.ts b/companion/lib/Surface/Group.ts index 750f4a640f..e2460f6a03 100644 --- a/companion/lib/Surface/Group.ts +++ b/companion/lib/Surface/Group.ts @@ -29,6 +29,9 @@ export class SurfaceGroup { last_page_id: '', startup_page_id: '', use_last_page: true, + swipe_can_change_page: true, + restrict_pages: false, + allowed_page_ids: [], } /** @@ -267,11 +270,21 @@ export class SurfaceGroup { } } else { let newPage: string | null = toPage + const validPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] + const getNewPage = (currentPage: string, offset: number) => { + if (validPages.length > 0) { + let newPage = (validPages.indexOf(currentPage) + offset) % validPages.length + if (newPage < 0) newPage = validPages.length - 1 + return validPages[newPage] + } else { + return this.#pageStore.getOffsetPageId(currentPage, offset) + } + } // note: getOffsetPageId() calculates the new page with wrap around if (newPage === '+1') { - newPage = this.#pageStore.getOffsetPageId(currentPage, 1) + newPage = getNewPage(currentPage, 1) } else if (newPage === '-1') { - newPage = this.#pageStore.getOffsetPageId(currentPage, -1) + newPage = getNewPage(currentPage, -1) } else { newPage = String(newPage) } @@ -427,6 +440,19 @@ export function validateGroupConfigValue(pageStore: IPageStore, key: string, val return value } + case 'swipe_can_change_page': + case 'restrict_pages': + return Boolean(value) + + case 'allowed_page_ids': { + const values = value as string[] + const errors = values.filter((page) => !pageStore.isPageIdValid(page)) + if (errors.length > 0) { + throw new Error(`Invalid allowed_page_ids "${errors.join(',')}"`) + } + + return values + } default: throw new Error(`Invalid SurfaceGroup config key: "${key}"`) } diff --git a/shared-lib/lib/Model/Surfaces.ts b/shared-lib/lib/Model/Surfaces.ts index 6e97433f23..d4eb604139 100644 --- a/shared-lib/lib/Model/Surfaces.ts +++ b/shared-lib/lib/Model/Surfaces.ts @@ -65,11 +65,16 @@ export interface SurfaceGroupConfig { last_page_id: string startup_page_id: string use_last_page: boolean + swipe_can_change_page?: boolean + restrict_pages?: boolean + allowed_page_ids?: string[] - /** @deprecated. replaced by last_page_id */ + /** @deprecated. replaced by last_page_id but still used for export */ last_page?: number - /** @deprecated. replaced by startup_page_id */ + /** @deprecated. replaced by startup_page_id but still used for export */ startup_page?: number + /** @deprecated. used for export */ + allowed_pages?: number[] } export interface SurfacePanelConfig { diff --git a/webui/src/Controls/InternalModuleField.tsx b/webui/src/Controls/InternalModuleField.tsx index 0f00d27540..ba59e20bcc 100644 --- a/webui/src/Controls/InternalModuleField.tsx +++ b/webui/src/Controls/InternalModuleField.tsx @@ -185,6 +185,7 @@ interface InternalPageIdDropdownProps { value: any setValue: (value: any) => void disabled: boolean + multiple?: boolean } export const InternalPageIdDropdown = observer(function InternalPageDropdown({ @@ -194,6 +195,7 @@ export const InternalPageIdDropdown = observer(function InternalPageDropdown({ value, setValue, disabled, + multiple, }: InternalPageIdDropdownProps) { const { pages } = useContext(RootAppStoreContext) @@ -216,7 +218,11 @@ export const InternalPageIdDropdown = observer(function InternalPageDropdown({ return choices }, [pages, /*isLocatedInGrid,*/ includeStartup, includeDirection]) - return + if (multiple === undefined || !multiple) { + return + } else { + return + } }) interface InternalCustomVariableDropdownProps { diff --git a/webui/src/Surfaces/EditPanel.tsx b/webui/src/Surfaces/EditPanel.tsx index ea0fc40f24..3aa0fd18bb 100644 --- a/webui/src/Surfaces/EditPanel.tsx +++ b/webui/src/Surfaces/EditPanel.tsx @@ -355,10 +355,10 @@ const SurfaceEditPanelContent = observer(function Surf /> - {(surfaceId === null || !!surfaceInfo?.isConnected || !!groupConfig.config.use_last_page) && ( + {(surfaceInfo === null || !!surfaceInfo.isConnected || !!groupConfig.config.use_last_page) && ( <> - {surfaceId === null || surfaceInfo?.isConnected ? 'Current Page' : 'Last Page'} + {surfaceInfo === null || surfaceInfo?.isConnected ? 'Current Page' : 'Last Page'} (function Surf )} + + + Restrict pages accessible to this {surfaceId === null ? 'group' : 'surface'} + + + setGroupConfigValue('restrict_pages', !!e.currentTarget.checked)} + /> + + + {!!groupConfig.config.restrict_pages && ( + <> + + Allowed pages: + + + setGroupConfigValue('allowed_page_ids', val)} + /> + + + )} + + { + /*!(surfaceInfo && surfaceInfo.type.toLowerCase() === 'emulator') + TODO: Generalize the condition for showing lcd-strip swipes? */ + (!surfaceInfo || surfaceInfo.type.toLowerCase() === 'elgato stream deck +') && ( + <> + + Allow Swipe to Change Pages (SD+) + + + setGroupConfigValue('swipe_can_change_page', !!e.currentTarget.checked)} + /> + + + ) + } )} From 85eb3161d02eaca3190871a78ffde43b1659e65a Mon Sep 17 00:00:00 2001 From: arikorn Date: Sat, 18 Oct 2025 20:52:33 -0700 Subject: [PATCH 03/12] Update documentation --- docs/3_config/surfaces.md | 14 +++++---- docs/3_config/surfaces/groups.md | 28 ++++++++++++++---- .../surfaces/images/restrict_pages.png | Bin 0 -> 6961 bytes docs/7_surfaces/elgato_streamdeck.md | 8 ++++- 4 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 docs/3_config/surfaces/images/restrict_pages.png diff --git a/docs/3_config/surfaces.md b/docs/3_config/surfaces.md index ecedf134b9..6bcd3a8033 100644 --- a/docs/3_config/surfaces.md +++ b/docs/3_config/surfaces.md @@ -10,13 +10,17 @@ Click the **Settings** button next to a device to change how that surface behave Note: the exact list of settings varies by surface type. +- **Surface Name/Group Name**: A name for easy identification. - **Surface Group**: Assign this surface to a [Surface Group](#3_config/surfaces/groups.md). - **Use Last Page At Startup**: Whether the surface should remember the last page it was on at startup, or use the Startup Page setting instead. -- **Startup Page**: If 'Use Last Page At Startup' is not enabled, the page the Surface will show at startup. -- **X Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid -- **Y Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid +- **Home Page/Startup Page**: If 'Use Last Page At Startup' is not enabled, the page the Surface will show at startup. +- **Restrict pages accessible to this group**: If enabled, a multi-select dropdown will allow you to select which page(s) the surface or group may access. See the [Surface Group](#3_config/surfaces/groups.md) instructions for additional details. (v4.2) +- **Allow Swipe to Change Pages (SD Plus)**: If enabled, swiping horizontally on the LCD Panel of a Stream Deck+ will change pages (v4.2) +- **Horizontal Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid +- **Vertical Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid - **Brightness**: The brightness of the buttons. -- **Button rotation**: If you've physically rotated your Surface, use this to match the button orientation. +- **Surface rotation**: If you've physically rotated your Surface, use this to match the button orientation. - **Never pin code lock**: Exclude this device from pin-code locking. -In the settings for each **Emulator**, you can configure additional behaviours. +In the settings for each **Emulator**, you can configure additional parameters such as the row and column count here. + diff --git a/docs/3_config/surfaces/groups.md b/docs/3_config/surfaces/groups.md index 46a03646be..7522e559dc 100644 --- a/docs/3_config/surfaces/groups.md +++ b/docs/3_config/surfaces/groups.md @@ -1,10 +1,28 @@ -Surface groups is a recent addition which allows for linking multiple surfaces to always be on the same page. -This is convenient for setups such as having two Stream Deck XLs side by side, which want to be treated as a single 16x4 device. +Surface groups, introduced in Companion v3.2, allow multiple surfaces to be linked for several uses. -To do this, use the **+ Add Group** button to create a new surface group. Then for each surface you want to add to the group, open its settings and change the surface group dropdown to the newly created group. +## Use Case 1: Linking Control Surfaces +All surfaces in a group are linked to the same page in Companion. This is convenient for setups such as having two Stream Deck XLs side by side, which want to be treated as a single 4x16 device. -Elsewhere in Companion, the actions which can change the page of a surface will no longer see the individual surfaces, and can instead pick the group. +To do this, use the **+ Add Group** button to create a new surface group. Then for each surface you want to add to the group, open its settings and change the surface group dropdown to the newly created group. +Finally, set the horizontal or vertical offset for each surface, to display the desired part of the Companion page. -Another use case for this, is that you can use a group as a stable id for a surface. +For example, in the *Settings > Buttons* page specify the grid size to be 4 x 16, then on the *Surfaces* page, put the two surfaces in a group, as described above, then select the line for the second Stream Deck XL in the group, and set *Horizontal Offset in grid* to 8: the first XL will display columns 0-7; the second XL will show columns 8-15 of your pages. + +Elsewhere in Companion, the actions that can change the page of a surface will no longer see the individual surfaces, and can instead pick the group. + +## Use Case 2: Addressing a surface without being tied to its serial number +A second use case for groups, is that you can use a group as a stable id for a surface. This helps setups where you need to be able to swap out a Stream Deck quickly, and also need actions to reference specific Stream Decks. You can create your groups and program the system ahead of time, then later add or remove surfaces to the groups without needing to update any buttons. + +## Use Case 3: Establishing "Permission Groups" with restricted access +Starting with v4.2, groups -- or ungrouped surfaces -- now provide the option to restrict which pages a device in that group may access. For example: + +--- +![Restrict Pages Setting](images/restrict_pages.png?raw=true 'Discovery') +--- + +This allows you to distribute a single site configuration but with access to particular pages defined by a person's role. Each group can be limitted to just one or a few pages relevant to their role. +Any surface "registered" to (i.e. included in) a particular group will then be able to access only the pages allowed for that surface group. + +*Tip*: Next/previous page buttons, swiping, and actions will follow the order selected here. \ No newline at end of file diff --git a/docs/3_config/surfaces/images/restrict_pages.png b/docs/3_config/surfaces/images/restrict_pages.png new file mode 100644 index 0000000000000000000000000000000000000000..dc18b7718fa96ada6c37e127036d8b7ed22d4280 GIT binary patch literal 6961 zcmb`MS5#A9m%t;UAWAP5n!=wVAVs7~FQO1S0qG!3Kmm~d(U2HowN68d+*=5iADxG^mMoA0002}lgC;n z0031SrR+s}nevu*@)4taF8G<~XaHb?T;nVd`N4W3N_|UVq^TU~emvxvIK`3yZ#ccRFYUlFd=1y?YK=oCs3ltO8Zl|QC zt}9r_pRrOT02N&{6XkPzo{9EvEfvH6gSgKacX5D?F8+Rbi>*6S{q8@NmPL(eA&(CD zggFyAi0C+?zhE1fqHV=tguT;Q$9cbaC~n|GoShNZx~=~bHA^mLWXv={?Fs-OX&Fz` z>BA{ta#uI8s!{!(E<^cPKXq9`!T~mjZ%hC^W2pZ-#F(L~qqHd2B=gqU*`Uowx5z;+6^eCiq zuz8TEcnK^RO2UZbq1E9(f)3L>@mv#@`UD=d6%NS}uId_!N~$K^T#s*Wo5)`Y{(&2R z?0d}jWJ|QCVVsn4T(qJhL|K^!uYTWs%n>zF-q6vTB4{I%bz9@O|$^h{LkzJD$? zeZ3N{+#9hyFXzi83~X}n^>|S1<^1;kMh;(7{!kb3k5XhXRhAZe89T)SKH&}p19ZO`y?1b(fCM_*thnKPbIUcTN(^u$` zDgzzH8`;wZc=>C8h88+hDimL`%MhmZrxwQJ&d(TzUn0+;UW?B)?mr5Z|7s>@j8YLd zT(ts8H#fWWp3NB7tLdRwOrs5FIlV_kw;!)1CnCKzZUrOH{mpB(CN_h0jS5;GD|RYK zZTihS5K9?!Ru%TU(enDCiz30naOJZ){Leq+HvNwA2(o?_;{9YxAgRSxmG!$C&=yLN zE@_n8iO&qPKR`Sn&fKg8H!k7&+uZgFFH!3O08xNEA6|`{4b{3i4xo|Oc-tSg_fo<} zsqdLO_L~drKd(gJTCRpzcCEIZu(@6@FRKq}X!+1zz3e{X4^pcMO*28#+K<>06ibE8 zRUcuv277(X;e22FsDYU3d*g0ZZ<-4hXFy+V&>bIDu`PXguC22`8|BPif3q{l_{(u3 zbH$@@J4;7a^%n?+squ)VYnSU<+02Z$Sd=%rTGCTQUY}KyQeh1m9-!1s?t7lN+3*|# z6>|8k)6T+!0z-?D1q`^P3$e=P#?rg!v|oG76(&yE_p%Fg#q7IUL%wo7%}_I?zMmN& zcLY>xi%}o;%qe{6A=HcS8-We`pigbYteSm6WKa3!Nc~Qq8TZU0n>6n0A@K{u z+0`Ul4Z80J5}bAqf5h8Hy)}Q^1B3*+0Q(4B8hWqiQ+8^>gs7)D+9mTcG_Wwt#TAeq zj^6W!sDf`QJp*Y>{!*#z{$MQN2NK*0eMNvm?eqy7%tz*!TxoeI z$s&`^D6NArtv$}ND+8o}KY0IZj|6?)%33!bo4uT!S-G z#x3A&kXPOXIK`ek(svF{`mqwK|p+C4=IoBo~pi zH{1r*lCm(#djE$F{x$`hH|UW8kvfSa5k;tf(UK%AC4a-7r%B=tk?e=);!F+*-BIZE zo5zQWB$c1^{eD6uFSC@*iiKbLJku;1v@mYhTkl&hV;yPG&bU**x)>pzu|tN?d9V5v zMsRh{o@&GmGL$9pP`jbQhWO31H?YuyIpKrF^p$RWTgrr#iV(N}exaBNaJ$LXmgyi2 z;2WO?HRrmq@BN{_hsVD^R_Ya_^M&~hE}FV$) z1;!!j=b`!aPhhzLJpgbWT3ain>{fskcJh&_*>}#jkf!kkH2Iz*4I@+`_Uve|99f#I z?HxES`6GziyKDMZ*u6DVS^yJ}Hcq=DUr3KQt>_@Q$lQ*Ke5Ny8kcg`}^Mt&ZRYSmS zKIJ&~PVY9fF)n#7cy_Ki0ix!w1Z7HnJD7)uFT8K>VThAajYxx?PR*@b94inq&x;q} zbD}uW)xomO_SRI`@wnzbWr1wMhMcb%2^x@>drA^IM9ssD54pwIcDhLS+XdTY0RUPK zYK?Cg4>pqDJ@GT`ljaHK06%DSyCE=FNxp(ARoN?Gd5g28resWUX z`S#$*Onz>TDHFw*-u?t$xAWimZ`570AMsriP+^G+4dysV|Iz2*Q-O0iiElN zbf(qtAp7qV-4m3YrQ?2|+7Hx8YNVzPHz8$Bn`hj6|M`C?1ay!%1xW_wD|!+GYEz_LOXd+At#8r!-+hBb6;jB4ez51b$`m0dH~@ETKQQ zCq-~6+LbWnW-Z}qFZFv%ca6^FZ9Yv<&FAKj97Wd|@$i3k6f_2&d3Kp84){_BM;-DV#n~*#Bb;)h1z=R{>{%~qeX^_O)nc&S_f4S7yFHoRdddNn zWOp~ZlAy(UBi{L84e_cWSKGLy^-8z8*ob4!KbhRAW82onY`E<&>31*Md{n;rswi2PYIbP~l zZrORUx*fVTtlR75k;+)c1Orn>Gvm#VI=!;q^?unAJ$uzr%W_+Y;n}Z(W#P@v-Cn6!t}=7y(a(ay}mteSH7n&Wjj0yJ>C#_!(W_;zNtFX zQxrFcmxs)0K5W>VHtZgl5(sH9((|bKl86_;Cg7S^zYJU+$Ny_ZxkaKkT-@IWd}2tpwR*rhD1V1GT(m?$3?T zA=l`kUL)}CZXT7(3Ei{(#+$6N=Xu^L7zH@0cr*sjO_hS5# zIyy%tbWDoTBX7#VwNQ^*BP^2dZ}-6O-@hMxtXU4H#jhTVt8Z@F-;({TOGBwL(pur9 z4gUd$sP`~X#7~v3z)UGh0RWus_qhKTKB6+zZd2kQfL5Mb~5(XRvFOHC#?sl{>=6s_N*{M^oAc0j>~l zYPnDVhpq$6d3r3xeUX{@-#JI}=Z#N4_|jRtn*Aketm<_o`nI~Hc}LbqE7nhg!(;}` zs&Z}BEo^)**#=--LK0)#>~CR;Q~}OYt$`9=s!d5ObHi850grB8%(_FJ0X&VY>iPKn z``{>P2PLtJ-Tgy8R_^Kp{%w)P_&0LAQ8T%P(-z+AL`ZO>aEy{O47@7UIKcl2x&)bM zS;x5Oj2{~eEM{AmXxBievk$|SkrY;{fK>#mNl*z}CsayX>G+S2-TkoaQ<5&b8(`a( z9cT|G4;((*P^!#nPBEFIu#4YAA6xCbn>!l!=z%#8;0=tBkEo8X9i9y--Ebs}drq@_N6_q5AzfD|h1T)+J9y z>0w*de(Rz5$T>ez44x(`Lqv&*p<5W{_N2dusTxoYTs!00NGF<}e_&ty9WnKRKa1-G zKJ&l^@g^icXH$R*wMqtCYRa^s_@h$Wi#|Cjqt0c}zAo=POJ+)mIi`#?RpXoVeqluc zvPNZgf7mc3<3y}Ie@55K5VJQKx*n6w+2&8-_`t(II{Q#Qo`@sIn`%KG?!iyRNo$|e z56m;=Ld3MJ3CKRrxyE~*L7!vMBijqNtnk3A7(=lACQdM54mHC(#UR+-T72#NG)jmK z`lZQhj&Ac*$qQ);Z^&K3AF#=YjU;oAu+`4;&PE<3=!;O#PVw!ypC9_333cgI6{)H< zEBed>wy@2aX}qei9On>RVc@bX9Cg-WXL3e&EzW%d)8{J*9Deh~h9weX%}*V*_|$-& zxc1iHGftjmOqJxYH0`7N!$gO4nc$rxeoqvp4+JixU7*@-jLL#PeF%V`)^oZe>0wWI99J8r|sRuW(dWQ6?uR4DCY{9fCeUR zpRuc!N5qVgYQaVKB;Vz19AC^5WeCCqa7WieViG4 zGL%g92y@lfYmL^DT^?|z3n8HfatfPOFrCQ9F0$)Z)A?Q$+!p_(;_*t(=^+}_`0M53 zu3LWBW~JLddZR~b2!AE%-~zC|bh8ysseJaEVtP?^D04sAGE*@ySO%%tx1MCu@T~$8 zF2i;l_JMJkl}5N@-%AZ$-)`L*_e}Oa$UO432Q9 zl9HIgj|%*;bGu(oT6;{YvTVL@iZ?H_oNQR2AQM@r7%In;>>GVblSYQ`H-4K*usS*p zeCK+>$GtDy;QUbASr}3>CEzhUBzexCU9Rkl^xCKGlb3s~Wv40MV)bI6LquZ>*40yG zOe4k+JLo6y@RstlYi8+Tb}FpgcQdz3)8oBnSRwb}(b+B1m{Ky_k4NWSG#ZhzgVO%4 z;$eq@6-h-o5H3u&gUw&8S5yFdhAh}Y!J6H>FFh8?yh8(hEsMBo@5*2048N#uCKU&t zK=PLGS!D{-2Z5d^clrbeIc%k}AR3FU6}17%NB`Vd}GW9m)ro= zZ^YTsf=qt|jDdLn#^z6NS48dVdhgmc-YpGnx?OG`!PwaMsrk(|q9{@T@}{P&LUuc7R(4ibU$S5yoQ_+256{nk`iOHmZEcl%F{vga1G%rt%Em;y<-=OelLEs4KW`2FWRDQ;RF%=6m|6ozLF*)w?D8q_nrDZ@R*Y?%ZN2 z7Tv4!sDqtn9NBw~f~ByJt^`+8*YB10-PmG2|B<&z-zbR1{fzYwhc{H|Rw|V@ZPt@d zo~GdwX#=%ZwZNp!0PrxwZ{*31lYFIYb5efmTW~RCe-K8S!Rh zx`!sRI94)@bd;Br&A`UR5nKz!OuEAEAQ%hgmD&+IC0+Zj$Oye`Mf<^yo5zVg&kj1T zN(>Fi%>jRXxc3}6;pwkeC3&p zwLf(1pI3@F^juOCzqnOsjX1=Mnf=R;ErLme&52(k^B9EN`g#TU!)PyWB+zfQzc`gs zZX{K1wz9G4CgrNn+)v5^X@)NYh5u^A1Zl-7DzKKFv@WMGm6VBChy?zIMm><%a=&?` zb(B1wbm>VFGUHRRx)dj@;`ieDiy9AZi%DlZ1YuR#S<22MfNrj%8#Pf8fWic%e_;n^ ziZ)8;$a$n;li1ZfnGGj-XgnoBgNRFFn2k3J2l|+7ZiWNM%a{WZ8nw5o3b%hqI(8Pl zl(jP}%g;tocVEG8(&QObNm>E6!3d|`%&$HR8X*V60UK*Uz(d>6)4g72o$V5-Y;_$M znd_r+5<)W*qc=-IlhOm6 zJ0v4ks(vfOiV*GclwCubV45j&D)O==d^Aeh0t)S#(9^DUNf;^#b`+{}>P1CJ`-E8L+ zxp7T%rTKmz(WBRgHZ0Z=D30TDh7j|3BYh8tB&RQlOIDtq5Xs7a-kAjON@1!uxe1%^ z0)>ZTIkc9IE^<9EMM{IzUqY7Q5KN;!(Rc~2&io4*%l$E&Hk?!|@Vvm%JKwIxrvz5E zdPbu2NGg07D84m0ys}YX${%xIqV;sYIi%0I>~4=`Qf%IDLqlNu1h|>s`Vs<`cid1d|=$jSGEY{%Ld-%_QxUy90Qd zTm^aBfY+~w4!5RV<~s5`{-T3hFTbbDmn){z(7}y@c}okDvP@-6EI%<&I##UTbCvtg z9jHs8IhIdhSA&LcJ9$DNrUYr272+?WzreJ@;Ck2`L|r#i&ADYTG*Y-xuK+7h_|TNI z9gmU$DE^6*hMi4z4sWoqsly=b@@yvm#a&lqiS#sdk;tM8`)e1N-(I z*p-d?WPa#~{-zA>=Ni^jru@nJ{@+YL_6QNM;$Dn|!-s$#h#gLlds zlbp^A&t$x!clqw)sTO)JMKi@vMGu(|O$>fJ!p0i>fq%Nv_)_Br1$UzA;?(3~Yb?d$3Qt`Z2YR-5?taA9QeXn0dm)<1A%7397E@&*^$@SHgvm*p`)kk2 zc%+Zwg17Lu2L*E8JTZwhjB3KG53J*uQSjJr2qKPnO-R}$^bBC6@-HZ$<94|Q05~{g z{U?cC{U7iWuG5oPkT|T_z1MdU0C4X5ixwSV{<1e}v_{TywtBBm`$4Z4sUxmZh-f+e z--x6H%zuKJo){gLn=5hZO)EC}-$>(_CH0N%r~@!8F)Occ-+t!Ra#$DW&QUnwNf{jW zzi Date: Mon, 20 Oct 2025 23:17:20 -0700 Subject: [PATCH 04/12] fix prettier complaints --- docs/3_config/surfaces.md | 1 - docs/3_config/surfaces/groups.md | 19 +++++++++++-------- docs/7_surfaces/elgato_streamdeck.md | 9 +++++---- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/3_config/surfaces.md b/docs/3_config/surfaces.md index 6bcd3a8033..38eeba2343 100644 --- a/docs/3_config/surfaces.md +++ b/docs/3_config/surfaces.md @@ -23,4 +23,3 @@ Note: the exact list of settings varies by surface type. - **Never pin code lock**: Exclude this device from pin-code locking. In the settings for each **Emulator**, you can configure additional parameters such as the row and column count here. - diff --git a/docs/3_config/surfaces/groups.md b/docs/3_config/surfaces/groups.md index 7522e559dc..5f95543461 100644 --- a/docs/3_config/surfaces/groups.md +++ b/docs/3_config/surfaces/groups.md @@ -1,28 +1,31 @@ Surface groups, introduced in Companion v3.2, allow multiple surfaces to be linked for several uses. ## Use Case 1: Linking Control Surfaces + All surfaces in a group are linked to the same page in Companion. This is convenient for setups such as having two Stream Deck XLs side by side, which want to be treated as a single 4x16 device. -To do this, use the **+ Add Group** button to create a new surface group. Then for each surface you want to add to the group, open its settings and change the surface group dropdown to the newly created group. -Finally, set the horizontal or vertical offset for each surface, to display the desired part of the Companion page. +To do this, use the **+ Add Group** button to create a new surface group. Then for each surface you want to add to the group, open its settings and change the surface group dropdown to the newly created group. +Finally, set the horizontal or vertical offset for each surface, to display the desired part of the Companion page. -For example, in the *Settings > Buttons* page specify the grid size to be 4 x 16, then on the *Surfaces* page, put the two surfaces in a group, as described above, then select the line for the second Stream Deck XL in the group, and set *Horizontal Offset in grid* to 8: the first XL will display columns 0-7; the second XL will show columns 8-15 of your pages. +For example, in the _Settings > Buttons_ page specify the grid size to be 4 x 16, then on the _Surfaces_ page, put the two surfaces in a group, as described above, then select the line for the second Stream Deck XL in the group, and set _Horizontal Offset in grid_ to 8: the first XL will display columns 0-7; the second XL will show columns 8-15 of your pages. Elsewhere in Companion, the actions that can change the page of a surface will no longer see the individual surfaces, and can instead pick the group. -## Use Case 2: Addressing a surface without being tied to its serial number +## Use Case 2: Addressing a surface without being tied to its serial number + A second use case for groups, is that you can use a group as a stable id for a surface. This helps setups where you need to be able to swap out a Stream Deck quickly, and also need actions to reference specific Stream Decks. You can create your groups and program the system ahead of time, then later add or remove surfaces to the groups without needing to update any buttons. ## Use Case 3: Establishing "Permission Groups" with restricted access + Starting with v4.2, groups -- or ungrouped surfaces -- now provide the option to restrict which pages a device in that group may access. For example: --- -![Restrict Pages Setting](images/restrict_pages.png?raw=true 'Discovery') ---- -This allows you to distribute a single site configuration but with access to particular pages defined by a person's role. Each group can be limitted to just one or a few pages relevant to their role. +## ![Restrict Pages Setting](images/restrict_pages.png?raw=true 'Discovery') + +This allows you to distribute a single site configuration but with access to particular pages defined by a person's role. Each group can be limited to just one or a few pages relevant to their role. Any surface "registered" to (i.e. included in) a particular group will then be able to access only the pages allowed for that surface group. -*Tip*: Next/previous page buttons, swiping, and actions will follow the order selected here. \ No newline at end of file +_Tip_: Next/previous page buttons, swiping, and actions will follow the order selected here. diff --git a/docs/7_surfaces/elgato_streamdeck.md b/docs/7_surfaces/elgato_streamdeck.md index 22132c6943..6bdcd33184 100644 --- a/docs/7_surfaces/elgato_streamdeck.md +++ b/docs/7_surfaces/elgato_streamdeck.md @@ -19,13 +19,14 @@ We recommend connecting Stream Decks without the Elgato software. If you use the The Stream Deck + has rotary encoders and a touch strip. -The touch strip is mapped to four button. These buttons provide *press* events to Companion without a separate *release* event; "pressing" (tapping) the strip generates both down and up actions with a short delay. Consequently, the touch strip cannot respond to long-press actions. +The touch strip is mapped to four button. These buttons provide _press_ events to Companion without a separate _release_ event; "pressing" (tapping) the strip generates both down and up actions with a short delay. Consequently, the touch strip cannot respond to long-press actions. + +The touch strip also supports two types of swipe responses: -The touch strip also supports two types of swipe responses: - swiping horizontally will change the page: swipe left to get the next page; right to get the previous page (as if you're dragging a piece of paper that is just under the surface.) To enable or disable page-turning, [change the settings in the Surfaces tab](#3_config/surfaces.md). -- swiping vertically: up emits *rotate-right*"*; down emits *rotate-left* (enable the `Enable Rotary Actions` checkbox for the button to access these events). +- swiping vertically: up emits _rotate-right_"*; down emits *rotate-left\* (enable the `Enable Rotary Actions` checkbox for the button to access these events). -Note that the LCD Strip may occasionally confuse a vertical swipe with a press, so best practice may be to chose either *rotation* actions or *press* actions for the LCD buttons but not both at once. Or else to avoid *press actions* that change something critical, if using both. +Note that the LCD Strip may occasionally confuse a vertical swipe with a press, so best practice may be to chose either _rotation_ actions or _press_ actions for the LCD buttons but not both at once. Or else to avoid _press actions_ that change something critical, if using both. To use the rotary encoders for a control, enable the `Enable Rotary Actions` checkbox for that control. This adds additional action groups used when rotating the encoder. From 01034271a687751305df4833f3c7ebcbfa3804f7 Mon Sep 17 00:00:00 2001 From: arikorn Date: Wed, 22 Oct 2025 23:40:59 -0700 Subject: [PATCH 05/12] Fix markdown formatting --- docs/7_surfaces/elgato_streamdeck.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/7_surfaces/elgato_streamdeck.md b/docs/7_surfaces/elgato_streamdeck.md index 6bdcd33184..6dc48b71b3 100644 --- a/docs/7_surfaces/elgato_streamdeck.md +++ b/docs/7_surfaces/elgato_streamdeck.md @@ -24,7 +24,7 @@ The touch strip is mapped to four button. These buttons provide _press_ events t The touch strip also supports two types of swipe responses: - swiping horizontally will change the page: swipe left to get the next page; right to get the previous page (as if you're dragging a piece of paper that is just under the surface.) To enable or disable page-turning, [change the settings in the Surfaces tab](#3_config/surfaces.md). -- swiping vertically: up emits _rotate-right_"*; down emits *rotate-left\* (enable the `Enable Rotary Actions` checkbox for the button to access these events). +- swiping vertically: up emits _rotate-right_; down emits _rotate-left_ (enable the `Enable Rotary Actions` checkbox for the button to access these events). Note that the LCD Strip may occasionally confuse a vertical swipe with a press, so best practice may be to chose either _rotation_ actions or _press_ actions for the LCD buttons but not both at once. Or else to avoid _press actions_ that change something critical, if using both. From 9cdd291194d23209498a57fc9bbb69ef3a4711e4 Mon Sep 17 00:00:00 2001 From: arikorn Date: Sun, 26 Oct 2025 14:59:03 -0700 Subject: [PATCH 06/12] Tighten up page-restriction enforcement in Groups.ts - ensure as best as possible that page restrictions are enforced (a) at startup and (b) even if enabled after visiting now-restricted pages. --- companion/lib/Surface/Group.ts | 58 +++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/companion/lib/Surface/Group.ts b/companion/lib/Surface/Group.ts index e2460f6a03..342e95d1aa 100644 --- a/companion/lib/Surface/Group.ts +++ b/companion/lib/Surface/Group.ts @@ -149,8 +149,8 @@ export class SurfaceGroup { } // validate the current page id - if (!this.#pageStore.isPageIdValid(this.#currentPageId)) { - this.#currentPageId = this.#pageStore.getFirstPageId() + if (!this.#isPageIdValid(this.#currentPageId)) { + this.#currentPageId = this.#getFirstPageId() // Update the config to match this.groupConfig.last_page_id = this.#currentPageId @@ -246,6 +246,39 @@ export class SurfaceGroup { this.setCurrentPage(decrease ? '-1' : '+1') } + /** + * Get first page ID, taking into account `groupConfig.allowed_page_ids` + */ + #getFirstPageId(): string { + const validPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] + return validPages.length > 0 ? validPages[0] : this.#pageStore.getFirstPageId() + } + + /** + * Check if a page ID is valid, taking into account `groupConfig.allowed_page_ids` + */ + #isPageIdValid(pageID: string): boolean { + const allowedPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] + if (!this.#pageStore.isPageIdValid(pageID)) { + return false + } else if (allowedPages.length > 0) { + const isAllowed = allowedPages.includes(pageID) + if (!isAllowed && this.#pageHistory) { + const pageHistory = this.#pageHistory + // if page is valid but not on the "allowed" list: clean up history + if (pageHistory.history.some((p) => !allowedPages.includes(p))) { + pageHistory.history = pageHistory.history.filter((p) => allowedPages.includes(p)) + // not sure what's best here, but this should do... + pageHistory.index = pageHistory.history.length - 1 + } + } + return isAllowed + } else { + // page is valid and there are no page restrictions + return true + } + } + /** * Change the page of a surface, keeping a history of previous pages */ @@ -253,6 +286,8 @@ export class SurfaceGroup { const currentPage = this.#currentPageId const pageHistory = this.#pageHistory + const validPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] + if (toPage === 'back' || toPage === 'forward') { // determine the 'to' page const pageDirection = toPage === 'back' ? -1 : 1 @@ -260,27 +295,28 @@ export class SurfaceGroup { const pageTarget = pageHistory.history[pageIndex] // change only if pageIndex points to a real page + // note that in all common situations, the only way pageTarget is undefined + // is when we're trying to go beyond the beginning or end of history, + // in which case, doing nothing is the correct action. if (pageTarget !== undefined) { pageHistory.index = pageIndex - - // TODO - should this search for the first valid pageId? - if (!this.#pageStore.isPageIdValid(pageTarget)) return + if (!this.#isPageIdValid(pageTarget)) return this.#storeNewPage(pageTarget, defer) } } else { let newPage: string | null = toPage - const validPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] const getNewPage = (currentPage: string, offset: number) => { if (validPages.length > 0) { let newPage = (validPages.indexOf(currentPage) + offset) % validPages.length if (newPage < 0) newPage = validPages.length - 1 return validPages[newPage] } else { + // note: getOffsetPageId() calculates the new page with wrap around return this.#pageStore.getOffsetPageId(currentPage, offset) } } - // note: getOffsetPageId() calculates the new page with wrap around + // note: getNewPage() calculates the new page with wrap around if (newPage === '+1') { newPage = getNewPage(currentPage, 1) } else if (newPage === '-1') { @@ -288,7 +324,7 @@ export class SurfaceGroup { } else { newPage = String(newPage) } - if (!newPage || !this.#pageStore.isPageIdValid(newPage)) newPage = this.#pageStore.getFirstPageId() + if (!newPage || !this.#isPageIdValid(newPage)) newPage = this.#getFirstPageId() // Change page this.#storeNewPage(newPage, defer) @@ -312,9 +348,9 @@ export class SurfaceGroup { * Update the current page if the total number of pages change */ #pageCountChange = (_pageCount: number): void => { - if (!this.#pageStore.isPageIdValid(this.#currentPageId)) { + if (!this.#isPageIdValid(this.#currentPageId)) { // TODO - choose a better value? - this.#storeNewPage(this.#pageStore.getFirstPageId(), true) + this.#storeNewPage(this.#getFirstPageId(), true) } } @@ -448,7 +484,7 @@ export function validateGroupConfigValue(pageStore: IPageStore, key: string, val const values = value as string[] const errors = values.filter((page) => !pageStore.isPageIdValid(page)) if (errors.length > 0) { - throw new Error(`Invalid allowed_page_ids "${errors.join(',')}"`) + throw new Error(`Invalid allowed_page_ids values: [${errors.join(',')}]`) } return values From ed101b123b4cafea5fe4e2682414b8817e951ead Mon Sep 17 00:00:00 2001 From: arikorn Date: Sun, 26 Oct 2025 15:02:31 -0700 Subject: [PATCH 07/12] Move "swipe" functions to ElgatoStreamDeck.ts --- companion/lib/Surface/Controller.ts | 11 ---------- companion/lib/Surface/Group.ts | 2 -- companion/lib/Surface/Handler.ts | 20 +++++++----------- companion/lib/Surface/Types.ts | 2 +- companion/lib/Surface/USB/ElgatoStreamDeck.ts | 16 ++++++++++++-- shared-lib/lib/Model/Surfaces.ts | 1 - webui/src/Surfaces/EditPanel.tsx | 21 ------------------- 7 files changed, 23 insertions(+), 50 deletions(-) diff --git a/companion/lib/Surface/Controller.ts b/companion/lib/Surface/Controller.ts index c38641cba3..a931aa22ec 100644 --- a/companion/lib/Surface/Controller.ts +++ b/companion/lib/Surface/Controller.ts @@ -1571,17 +1571,6 @@ export class SurfaceController extends EventEmitter { return undefined } } - /** - * Get permission for swipe to change pages - */ - deviceAllowsSwipeToChangePage(surfaceOrGroupId: string, looseIdMatching = false): boolean | undefined { - const surfaceGroup = this.#getGroupForId(surfaceOrGroupId, looseIdMatching) - if (surfaceGroup) { - return surfaceGroup.groupConfig.swipe_can_change_page - } else { - return undefined - } - } /** * Get the groupId for a surfaceId (or groupId) diff --git a/companion/lib/Surface/Group.ts b/companion/lib/Surface/Group.ts index 342e95d1aa..e130196e30 100644 --- a/companion/lib/Surface/Group.ts +++ b/companion/lib/Surface/Group.ts @@ -29,7 +29,6 @@ export class SurfaceGroup { last_page_id: '', startup_page_id: '', use_last_page: true, - swipe_can_change_page: true, restrict_pages: false, allowed_page_ids: [], } @@ -476,7 +475,6 @@ export function validateGroupConfigValue(pageStore: IPageStore, key: string, val return value } - case 'swipe_can_change_page': case 'restrict_pages': return Boolean(value) diff --git a/companion/lib/Surface/Handler.ts b/companion/lib/Surface/Handler.ts index 996e54fec3..33b2407356 100644 --- a/companion/lib/Surface/Handler.ts +++ b/companion/lib/Surface/Handler.ts @@ -259,7 +259,7 @@ export class SurfaceHandler extends EventEmitter { this.panel.on('click', this.#onDeviceClick.bind(this)) this.panel.on('rotate', this.#onDeviceRotate.bind(this)) - this.panel.on('swipePage', this.#onDeviceSwipePage.bind(this)) + this.panel.on('changePage', this.#onDeviceChangePage.bind(this)) this.panel.on('pincodeKey', this.#onDevicePincodeKey.bind(this)) this.panel.on('remove', this.#onDeviceRemove.bind(this)) this.panel.on('resized', this.#onDeviceResized.bind(this)) @@ -583,22 +583,18 @@ export class SurfaceHandler extends EventEmitter { } } - #onDeviceSwipePage(forward: boolean): void { + #onDeviceChangePage(forward: boolean): void { if (this.#isSurfaceLocked || !this.panel) return const pageNumber = this.#pageStore.getPageNumber(this.#currentPageId) if (!pageNumber) return + const name = this.displayName - if (this.#surfaces.deviceAllowsSwipeToChangePage(this.surfaceId, true)) { - try { - this.#surfaces.devicePageSet(this.surfaceId, forward ? '+1' : '-1', true) - this.#logger.debug(`Swipe page ${pageNumber}: ${forward ? 'forward' : 'backward'}`) - } catch (e) { - this.#logger.error(`Swipe failed: ${e}`) - } - } else { - const name = this.#surfaces.getGroupNameFromDeviceId(this.surfaceId, true) - this.#logger.debug(`Swipe page ${pageNumber} disabled in Surface/Group config for "${name}".`) + try { + this.#surfaces.devicePageSet(this.surfaceId, forward ? '+1' : '-1', true) + this.#logger.debug(`Change page ${pageNumber}: ${forward ? 'forward' : 'backward'} initiated by ${name}`) + } catch (e) { + this.#logger.error(`Change page failed for ${name}: ${e}`) } } diff --git a/companion/lib/Surface/Types.ts b/companion/lib/Surface/Types.ts index 52ba0f2e31..172cb78035 100644 --- a/companion/lib/Surface/Types.ts +++ b/companion/lib/Surface/Types.ts @@ -73,7 +73,7 @@ export interface SurfacePanelEvents { click: [x: number, y: number, pressed: boolean, pageOffset?: number] rotate: [x: number, y: number, rightward: boolean, pageOffset?: number] - swipePage: [forward: boolean] + changePage: [forward: boolean] pincodeKey: [key: number] setCustomVariable: [variableId: string, value: CompanionVariableValue] diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index b72b01a126..2be0aa43b7 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -38,10 +38,22 @@ export const StreamDeckJpegOptions: JPEGEncodeOptions = { function getConfigFields(streamDeck: StreamDeck): CompanionSurfaceConfigField[] { const fields: CompanionSurfaceConfigField[] = [...OffsetConfigFields] + if (streamDeck.MODEL === DeviceModelId.PLUS) { + // place it above offset, etc. + fields.unshift({ + id: 'swipe_can_change_page', + label: 'Horizontal Swipe Changes Page', + type: 'checkbox', + default: true, + tooltip: 'Swiping horizontally on the Stream Deck+ LCD-strip will change pages, if enabled.', + } as CompanionSurfaceConfigField) + } + // Hide brightness for the pedal const hasBrightness = !!streamDeck.CONTROLS.find( (c) => c.type === 'lcd-segment' || (c.type === 'button' && c.feedbackType !== 'none') ) + // is it safe/advisable to be pushing the object instead of a copy? if (hasBrightness) fields.push(BrightnessConfigField) fields.push(LegacyRotationConfigField, ...LockConfigFields) @@ -311,9 +323,9 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter if (angle >= 50 && toButton === fromButton) { //vertical swipe. note that y=0 is the top of the screen so for swipe up `from.y` is the higher this.emit('rotate', fromButton, control.row, from.y > to.y) - } else if (angle <= 22.5) { + } else if (angle <= 22.5 && this.config.swipe_can_change_page) { // horizontal swipe, change pages: (note that the angle of the SD+ screen diagonal is 7 degrees, i.e. atan 1/8) - this.emit('swipePage', from.x > to.x) //swipe left moves to next page, as if your finger is moving a piece of paper under the screen + this.emit('changePage', from.x > to.x) //swipe left moves to next page, as if your finger is moving a piece of paper under the screen } }) } diff --git a/shared-lib/lib/Model/Surfaces.ts b/shared-lib/lib/Model/Surfaces.ts index d4eb604139..e4ac4b2b06 100644 --- a/shared-lib/lib/Model/Surfaces.ts +++ b/shared-lib/lib/Model/Surfaces.ts @@ -65,7 +65,6 @@ export interface SurfaceGroupConfig { last_page_id: string startup_page_id: string use_last_page: boolean - swipe_can_change_page?: boolean restrict_pages?: boolean allowed_page_ids?: string[] diff --git a/webui/src/Surfaces/EditPanel.tsx b/webui/src/Surfaces/EditPanel.tsx index 3aa0fd18bb..76b24f95d6 100644 --- a/webui/src/Surfaces/EditPanel.tsx +++ b/webui/src/Surfaces/EditPanel.tsx @@ -402,27 +402,6 @@ const SurfaceEditPanelContent = observer(function Surf )} - - { - /*!(surfaceInfo && surfaceInfo.type.toLowerCase() === 'emulator') - TODO: Generalize the condition for showing lcd-strip swipes? */ - (!surfaceInfo || surfaceInfo.type.toLowerCase() === 'elgato stream deck +') && ( - <> - - Allow Swipe to Change Pages (SD+) - - - setGroupConfigValue('swipe_can_change_page', !!e.currentTarget.checked)} - /> - - - ) - } )} From 069a1ba3e1a5929af4e6ca604b04462c9839ded7 Mon Sep 17 00:00:00 2001 From: arikorn Date: Sun, 26 Oct 2025 15:44:58 -0700 Subject: [PATCH 08/12] Fix inconsistencies in handling of panel configFields * Ensure all `configFields` properties are added to to panelConfig * Ensure that defaults specified in `configFields` are honored * Ensure that missing fields are added when importing a preexisting export. * Deal with TypeScript oddities --- companion/lib/Instance/ConfigFields.ts | 1 + companion/lib/Surface/Config.ts | 14 ++++++++++++++ companion/lib/Surface/Handler.ts | 9 +++++++++ shared-lib/lib/Model/Options.ts | 1 + 4 files changed, 25 insertions(+) diff --git a/companion/lib/Instance/ConfigFields.ts b/companion/lib/Instance/ConfigFields.ts index b76d7b1270..fdee684d92 100644 --- a/companion/lib/Instance/ConfigFields.ts +++ b/companion/lib/Instance/ConfigFields.ts @@ -245,6 +245,7 @@ function translateCustomVariableField( ...translateCommonFields(field), type: 'custom-variable', width: width, + default: undefined, // type-check complains otherwise } } diff --git a/companion/lib/Surface/Config.ts b/companion/lib/Surface/Config.ts index 9c144bf4b5..5af30f163e 100644 --- a/companion/lib/Surface/Config.ts +++ b/companion/lib/Surface/Config.ts @@ -25,12 +25,26 @@ export function createOrSanitizeSurfaceHandlerConfig( let panelConfig = existingConfig?.config if (!panelConfig) { panelConfig = cloneDeep(PanelDefaults) + // add properties & defaults from their UI definitions (so a redundant `getDefaultConfig()` is not needed) + for (const cfield of panel.info.configFields) { + if (!(cfield.id in panelConfig) || 'default' in cfield) { + Object.assign(panelConfig, { [cfield.id]: cfield.default }) + } + } + // if `panel.getDefaultConfig() is present, let it override the previous sources... if (typeof panel.getDefaultConfig === 'function') { Object.assign(panelConfig, panel.getDefaultConfig()) } panelConfig.xOffset = Math.max(0, gridSize.minColumn) panelConfig.yOffset = Math.max(0, gridSize.minRow) + } else { + // check for new properties but don't change existing fields + for (const cfield of panel.info.configFields) { + if (!(cfield.id in panelConfig)) { + Object.assign(panelConfig, { [cfield.id]: cfield.default }) + } + } } if (panelConfig.xOffset === undefined || panelConfig.yOffset === undefined) { diff --git a/companion/lib/Surface/Handler.ts b/companion/lib/Surface/Handler.ts index 33b2407356..c50dd58a96 100644 --- a/companion/lib/Surface/Handler.ts +++ b/companion/lib/Surface/Handler.ts @@ -743,6 +743,15 @@ export class SurfaceHandler extends EventEmitter { redraw = true } + // if this is an import, the config file may have been missing fields: + // (note: import does not call `createOrSanitizeSurfaceHandlerConfig`, which might be a better option?) + for (const cfield of this.panel.info.configFields) { + if (!(cfield.id in newconfig)) { + Object.assign(newconfig, { [cfield.id]: cfield.default }) + redraw = true + } + } + this.#surfaceConfig.config = newconfig this.#saveConfig() diff --git a/shared-lib/lib/Model/Options.ts b/shared-lib/lib/Model/Options.ts index 310e6cd940..3f5e94cd71 100644 --- a/shared-lib/lib/Model/Options.ts +++ b/shared-lib/lib/Model/Options.ts @@ -190,6 +190,7 @@ export interface CompanionInputFieldCheckboxExtended extends CompanionInputField } export interface CompanionInputFieldCustomVariableExtended extends CompanionInputFieldBaseExtended { type: 'custom-variable' + default?: never // needed to avoid TypeScript errors (all other input fields have default props) } export type ExtendedInputField = From 96255f1690f20a828f08edf13c9641ea1ff7e749 Mon Sep 17 00:00:00 2001 From: arikorn Date: Sun, 26 Oct 2025 15:45:26 -0700 Subject: [PATCH 09/12] Update documentation ("Getting Started Guide") --- docs/3_config/surfaces.md | 2 +- docs/3_config/surfaces/groups.md | 13 +++++++------ docs/3_config/surfaces/pagepermissions.md | 11 +++++++++++ docs/security.md | 8 ++++++++ docs/structure.json | 9 +++++++++ 5 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 docs/3_config/surfaces/pagepermissions.md create mode 100644 docs/security.md diff --git a/docs/3_config/surfaces.md b/docs/3_config/surfaces.md index 38eeba2343..cc9387638b 100644 --- a/docs/3_config/surfaces.md +++ b/docs/3_config/surfaces.md @@ -14,7 +14,7 @@ Note: the exact list of settings varies by surface type. - **Surface Group**: Assign this surface to a [Surface Group](#3_config/surfaces/groups.md). - **Use Last Page At Startup**: Whether the surface should remember the last page it was on at startup, or use the Startup Page setting instead. - **Home Page/Startup Page**: If 'Use Last Page At Startup' is not enabled, the page the Surface will show at startup. -- **Restrict pages accessible to this group**: If enabled, a multi-select dropdown will allow you to select which page(s) the surface or group may access. See the [Surface Group](#3_config/surfaces/groups.md) instructions for additional details. (v4.2) +- **Restrict pages accessible to this group**: If enabled, a multi-select dropdown will allow you to select which page(s) the surface or group may access. See the [Page Permissions](#3_config/surfaces/pagepermissions.md) instructions for additional details. (v4.2) - **Allow Swipe to Change Pages (SD Plus)**: If enabled, swiping horizontally on the LCD Panel of a Stream Deck+ will change pages (v4.2) - **Horizontal Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid - **Vertical Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid diff --git a/docs/3_config/surfaces/groups.md b/docs/3_config/surfaces/groups.md index 5f95543461..a92c90cf81 100644 --- a/docs/3_config/surfaces/groups.md +++ b/docs/3_config/surfaces/groups.md @@ -19,13 +19,14 @@ You can create your groups and program the system ahead of time, then later add ## Use Case 3: Establishing "Permission Groups" with restricted access -Starting with v4.2, groups -- or ungrouped surfaces -- now provide the option to restrict which pages a device in that group may access. For example: +Starting with v4.2, groups -- as well as ungrouped surfaces -- now provide the option to restrict which pages a device in that group may access. Using this in a surface-group configuration can be very useful if: ---- +1. Each surface is controlled independently (for example, each surface is controlled by its own computer or Raspberry Pi) -## ![Restrict Pages Setting](images/restrict_pages.png?raw=true 'Discovery') +...OR... -This allows you to distribute a single site configuration but with access to particular pages defined by a person's role. Each group can be limited to just one or a few pages relevant to their role. -Any surface "registered" to (i.e. included in) a particular group will then be able to access only the pages allowed for that surface group. +2. It is expected that all user in a group should be on the same page (for example, if users are restricted to a single page) -_Tip_: Next/previous page buttons, swiping, and actions will follow the order selected here. +By creating groups for each role, a single site configuration can be created, with page access permissions determined by the device or group ID. Any surface "registered" to (i.e. included in) a particular group will then be able to access only the pages allowed for that surface group. + +See: [Page Permissions](#3_config/surfaces/pagepermissions.md) for additional details. \ No newline at end of file diff --git a/docs/3_config/surfaces/pagepermissions.md b/docs/3_config/surfaces/pagepermissions.md new file mode 100644 index 0000000000..20a22c14e0 --- /dev/null +++ b/docs/3_config/surfaces/pagepermissions.md @@ -0,0 +1,11 @@ +## Page Permissions: Restricting access to pages by device or group + +Starting with v4.2, individual surfaces or surface-groups provide the option to restrict which pages a device -- or devices in a group -- may access. For example: + +--- + +## ![Restrict Pages Setting](images/restrict_pages.png?raw=true 'Discovery') + +This allows you to distribute a single site configuration but with access to particular pages defined by a person's role. Each device or group can then be limited to just one or a few pages relevant to their role. + +_Tip_: Next/Previous page-change actions -- including the standard page buttons, custom actions, and swiping (for surfaces that support it) -- will follow the order selected here. If you want to change the order, simply delete items from the list and re-select them in the order you prefer. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000000..b94d343e1a --- /dev/null +++ b/docs/security.md @@ -0,0 +1,8 @@ +Companion has a number of features that help you limit casual access to the admin interface and control surfaces. Although none of these features makes an installation secure, they can help to stop casual browsers. + +No single page of the Admin interface is dedicated to security, so this help section attempts to gather the features in a single place. + +1. [Admin Password](#3_config/settings/admin_password.md), _Settings > Advanced (Admin UI Password)_: If enabled, Companion will require a password to view any of the configuration pages +2. [Surface Lockscreen](#3_config/settings/pin_lockout.md), _Settings > Surfaces (PIN Lockout)_: Allows all connected surfaces to be locked out after a timeout and requires a PIN to unlock. +3. [Page Permissions](#3_config/surfaces/pagepermissions.md) _Surfaces_ tab: If enabled for a device or a group, it allows you to limit page access for that device/group. +4. [Remote Access](#5_remote_control.md) _Settings > Protocols_: Determine whether remote computers/devices can initiate a connection to Companion. \ No newline at end of file diff --git a/docs/structure.json b/docs/structure.json index 4bb1a7f85c..5792dfaf6f 100644 --- a/docs/structure.json +++ b/docs/structure.json @@ -90,6 +90,10 @@ { "label": "Discover", "file": "3_config/surfaces/discover.md" + }, + { + "label": "Page Permissions", + "file": "3_config/surfaces/pagepermissions.md" } ] }, @@ -161,6 +165,11 @@ } ] }, + { + "label": "Security Considerations", + "file": "security.md", + "children": [] + }, { "label": "Import / Export", "file": "3_config/import_export.md", From 1d9fcf8d8d06facb970c1acccfeb889cef28967f Mon Sep 17 00:00:00 2001 From: arikorn Date: Sun, 26 Oct 2025 16:10:05 -0700 Subject: [PATCH 10/12] Fix prettier issues --- docs/3_config/surfaces/groups.md | 4 ++-- docs/security.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/3_config/surfaces/groups.md b/docs/3_config/surfaces/groups.md index a92c90cf81..ce2b5cb2b6 100644 --- a/docs/3_config/surfaces/groups.md +++ b/docs/3_config/surfaces/groups.md @@ -19,7 +19,7 @@ You can create your groups and program the system ahead of time, then later add ## Use Case 3: Establishing "Permission Groups" with restricted access -Starting with v4.2, groups -- as well as ungrouped surfaces -- now provide the option to restrict which pages a device in that group may access. Using this in a surface-group configuration can be very useful if: +Starting with v4.2, groups -- as well as ungrouped surfaces -- now provide the option to restrict which pages a device in that group may access. Using this in a surface-group configuration can be very useful if: 1. Each surface is controlled independently (for example, each surface is controlled by its own computer or Raspberry Pi) @@ -29,4 +29,4 @@ Starting with v4.2, groups -- as well as ungrouped surfaces -- now provide the o By creating groups for each role, a single site configuration can be created, with page access permissions determined by the device or group ID. Any surface "registered" to (i.e. included in) a particular group will then be able to access only the pages allowed for that surface group. -See: [Page Permissions](#3_config/surfaces/pagepermissions.md) for additional details. \ No newline at end of file +See: [Page Permissions](#3_config/surfaces/pagepermissions.md) for additional details. diff --git a/docs/security.md b/docs/security.md index b94d343e1a..ccabc675f9 100644 --- a/docs/security.md +++ b/docs/security.md @@ -5,4 +5,4 @@ No single page of the Admin interface is dedicated to security, so this help sec 1. [Admin Password](#3_config/settings/admin_password.md), _Settings > Advanced (Admin UI Password)_: If enabled, Companion will require a password to view any of the configuration pages 2. [Surface Lockscreen](#3_config/settings/pin_lockout.md), _Settings > Surfaces (PIN Lockout)_: Allows all connected surfaces to be locked out after a timeout and requires a PIN to unlock. 3. [Page Permissions](#3_config/surfaces/pagepermissions.md) _Surfaces_ tab: If enabled for a device or a group, it allows you to limit page access for that device/group. -4. [Remote Access](#5_remote_control.md) _Settings > Protocols_: Determine whether remote computers/devices can initiate a connection to Companion. \ No newline at end of file +4. [Remote Access](#5_remote_control.md) _Settings > Protocols_: Determine whether remote computers/devices can initiate a connection to Companion. From ef859447fb2defc13a60f901d5a210c89424815f Mon Sep 17 00:00:00 2001 From: arikorn Date: Tue, 28 Oct 2025 19:23:18 -0700 Subject: [PATCH 11/12] Move page restrictions into a separate PR This branch is now strictly focused on SD+ swipe actions --- companion/lib/ImportExport/Controller.ts | 5 -- companion/lib/ImportExport/Export.ts | 3 - companion/lib/Surface/Controller.ts | 9 -- companion/lib/Surface/Group.ts | 82 +++--------------- companion/lib/Surface/Handler.ts | 2 +- docs/3_config/surfaces.md | 13 ++- docs/3_config/surfaces/groups.md | 30 +------ .../surfaces/images/restrict_pages.png | Bin 6961 -> 0 bytes docs/3_config/surfaces/pagepermissions.md | 11 --- docs/7_surfaces/elgato_streamdeck.md | 6 +- docs/security.md | 8 -- docs/structure.json | 9 -- shared-lib/lib/Model/Surfaces.ts | 8 +- webui/src/Controls/InternalModuleField.tsx | 8 +- webui/src/Surfaces/EditPanel.tsx | 35 +------- 15 files changed, 29 insertions(+), 200 deletions(-) delete mode 100644 docs/3_config/surfaces/images/restrict_pages.png delete mode 100644 docs/3_config/surfaces/pagepermissions.md delete mode 100644 docs/security.md diff --git a/companion/lib/ImportExport/Controller.ts b/companion/lib/ImportExport/Controller.ts index f09a3f6fd3..1de5f528ae 100644 --- a/companion/lib/ImportExport/Controller.ts +++ b/companion/lib/ImportExport/Controller.ts @@ -571,11 +571,6 @@ export class ImportExportController { groupConfig.startup_page_id = getPageId(groupConfig.startup_page!) delete groupConfig.startup_page } - if (groupConfig.allowed_pages !== undefined) { - const page_ids = groupConfig.allowed_pages.map((nr) => getPageId(nr)) - groupConfig.allowed_page_ids = page_ids - delete groupConfig.allowed_pages - } } // Convert external page refs, i.e. page numbers, to internal ids. diff --git a/companion/lib/ImportExport/Export.ts b/companion/lib/ImportExport/Export.ts index 56e827cbab..4b933cbcdb 100644 --- a/companion/lib/ImportExport/Export.ts +++ b/companion/lib/ImportExport/Export.ts @@ -570,9 +570,6 @@ export class ExportController { groupConfig.last_page = findPage(groupConfig.last_page_id) ?? 1 groupConfig.startup_page = findPage(groupConfig.startup_page_id) ?? 1 - if (groupConfig.allowed_page_ids !== undefined) { - groupConfig.allowed_pages = groupConfig.allowed_page_ids.map((id) => findPage(id) ?? 1) - } } for (const surface of Object.values(surfaces)) { diff --git a/companion/lib/Surface/Controller.ts b/companion/lib/Surface/Controller.ts index a931aa22ec..d171633937 100644 --- a/companion/lib/Surface/Controller.ts +++ b/companion/lib/Surface/Controller.ts @@ -1581,15 +1581,6 @@ export class SurfaceController extends EventEmitter { return surfaceGroup?.groupId } - /** - * Get the groupId for a surfaceId (or groupId) - */ - getGroupNameFromDeviceId(surfaceOrGroupId: string, looseIdMatching = false): string | undefined { - const surfaceGroup = this.#getGroupForId(surfaceOrGroupId, looseIdMatching) - - return surfaceGroup?.displayName - } - #resetAllDevices() { // Destroy any groups and detach their contents for (const surfaceGroup of this.#surfaceGroups.values()) { diff --git a/companion/lib/Surface/Group.ts b/companion/lib/Surface/Group.ts index e130196e30..750f4a640f 100644 --- a/companion/lib/Surface/Group.ts +++ b/companion/lib/Surface/Group.ts @@ -29,8 +29,6 @@ export class SurfaceGroup { last_page_id: '', startup_page_id: '', use_last_page: true, - restrict_pages: false, - allowed_page_ids: [], } /** @@ -148,8 +146,8 @@ export class SurfaceGroup { } // validate the current page id - if (!this.#isPageIdValid(this.#currentPageId)) { - this.#currentPageId = this.#getFirstPageId() + if (!this.#pageStore.isPageIdValid(this.#currentPageId)) { + this.#currentPageId = this.#pageStore.getFirstPageId() // Update the config to match this.groupConfig.last_page_id = this.#currentPageId @@ -245,39 +243,6 @@ export class SurfaceGroup { this.setCurrentPage(decrease ? '-1' : '+1') } - /** - * Get first page ID, taking into account `groupConfig.allowed_page_ids` - */ - #getFirstPageId(): string { - const validPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] - return validPages.length > 0 ? validPages[0] : this.#pageStore.getFirstPageId() - } - - /** - * Check if a page ID is valid, taking into account `groupConfig.allowed_page_ids` - */ - #isPageIdValid(pageID: string): boolean { - const allowedPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] - if (!this.#pageStore.isPageIdValid(pageID)) { - return false - } else if (allowedPages.length > 0) { - const isAllowed = allowedPages.includes(pageID) - if (!isAllowed && this.#pageHistory) { - const pageHistory = this.#pageHistory - // if page is valid but not on the "allowed" list: clean up history - if (pageHistory.history.some((p) => !allowedPages.includes(p))) { - pageHistory.history = pageHistory.history.filter((p) => allowedPages.includes(p)) - // not sure what's best here, but this should do... - pageHistory.index = pageHistory.history.length - 1 - } - } - return isAllowed - } else { - // page is valid and there are no page restrictions - return true - } - } - /** * Change the page of a surface, keeping a history of previous pages */ @@ -285,8 +250,6 @@ export class SurfaceGroup { const currentPage = this.#currentPageId const pageHistory = this.#pageHistory - const validPages = this.groupConfig.restrict_pages ? (this.groupConfig.allowed_page_ids ?? []) : [] - if (toPage === 'back' || toPage === 'forward') { // determine the 'to' page const pageDirection = toPage === 'back' ? -1 : 1 @@ -294,36 +257,25 @@ export class SurfaceGroup { const pageTarget = pageHistory.history[pageIndex] // change only if pageIndex points to a real page - // note that in all common situations, the only way pageTarget is undefined - // is when we're trying to go beyond the beginning or end of history, - // in which case, doing nothing is the correct action. if (pageTarget !== undefined) { pageHistory.index = pageIndex - if (!this.#isPageIdValid(pageTarget)) return + + // TODO - should this search for the first valid pageId? + if (!this.#pageStore.isPageIdValid(pageTarget)) return this.#storeNewPage(pageTarget, defer) } } else { let newPage: string | null = toPage - const getNewPage = (currentPage: string, offset: number) => { - if (validPages.length > 0) { - let newPage = (validPages.indexOf(currentPage) + offset) % validPages.length - if (newPage < 0) newPage = validPages.length - 1 - return validPages[newPage] - } else { - // note: getOffsetPageId() calculates the new page with wrap around - return this.#pageStore.getOffsetPageId(currentPage, offset) - } - } - // note: getNewPage() calculates the new page with wrap around + // note: getOffsetPageId() calculates the new page with wrap around if (newPage === '+1') { - newPage = getNewPage(currentPage, 1) + newPage = this.#pageStore.getOffsetPageId(currentPage, 1) } else if (newPage === '-1') { - newPage = getNewPage(currentPage, -1) + newPage = this.#pageStore.getOffsetPageId(currentPage, -1) } else { newPage = String(newPage) } - if (!newPage || !this.#isPageIdValid(newPage)) newPage = this.#getFirstPageId() + if (!newPage || !this.#pageStore.isPageIdValid(newPage)) newPage = this.#pageStore.getFirstPageId() // Change page this.#storeNewPage(newPage, defer) @@ -347,9 +299,9 @@ export class SurfaceGroup { * Update the current page if the total number of pages change */ #pageCountChange = (_pageCount: number): void => { - if (!this.#isPageIdValid(this.#currentPageId)) { + if (!this.#pageStore.isPageIdValid(this.#currentPageId)) { // TODO - choose a better value? - this.#storeNewPage(this.#getFirstPageId(), true) + this.#storeNewPage(this.#pageStore.getFirstPageId(), true) } } @@ -475,18 +427,6 @@ export function validateGroupConfigValue(pageStore: IPageStore, key: string, val return value } - case 'restrict_pages': - return Boolean(value) - - case 'allowed_page_ids': { - const values = value as string[] - const errors = values.filter((page) => !pageStore.isPageIdValid(page)) - if (errors.length > 0) { - throw new Error(`Invalid allowed_page_ids values: [${errors.join(',')}]`) - } - - return values - } default: throw new Error(`Invalid SurfaceGroup config key: "${key}"`) } diff --git a/companion/lib/Surface/Handler.ts b/companion/lib/Surface/Handler.ts index c50dd58a96..d5d7960c4d 100644 --- a/companion/lib/Surface/Handler.ts +++ b/companion/lib/Surface/Handler.ts @@ -592,7 +592,7 @@ export class SurfaceHandler extends EventEmitter { try { this.#surfaces.devicePageSet(this.surfaceId, forward ? '+1' : '-1', true) - this.#logger.debug(`Change page ${pageNumber}: ${forward ? 'forward' : 'backward'} initiated by ${name}`) + this.#logger.debug(`Change page ${pageNumber}: "${forward ? '+1' : '-1'}" initiated by ${name}`) } catch (e) { this.#logger.error(`Change page failed for ${name}: ${e}`) } diff --git a/docs/3_config/surfaces.md b/docs/3_config/surfaces.md index cc9387638b..ecedf134b9 100644 --- a/docs/3_config/surfaces.md +++ b/docs/3_config/surfaces.md @@ -10,16 +10,13 @@ Click the **Settings** button next to a device to change how that surface behave Note: the exact list of settings varies by surface type. -- **Surface Name/Group Name**: A name for easy identification. - **Surface Group**: Assign this surface to a [Surface Group](#3_config/surfaces/groups.md). - **Use Last Page At Startup**: Whether the surface should remember the last page it was on at startup, or use the Startup Page setting instead. -- **Home Page/Startup Page**: If 'Use Last Page At Startup' is not enabled, the page the Surface will show at startup. -- **Restrict pages accessible to this group**: If enabled, a multi-select dropdown will allow you to select which page(s) the surface or group may access. See the [Page Permissions](#3_config/surfaces/pagepermissions.md) instructions for additional details. (v4.2) -- **Allow Swipe to Change Pages (SD Plus)**: If enabled, swiping horizontally on the LCD Panel of a Stream Deck+ will change pages (v4.2) -- **Horizontal Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid -- **Vertical Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid +- **Startup Page**: If 'Use Last Page At Startup' is not enabled, the page the Surface will show at startup. +- **X Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid +- **Y Offset in grid**: If the device is smaller than the 8x4 grid, you can adjust the position of the surface within the grid - **Brightness**: The brightness of the buttons. -- **Surface rotation**: If you've physically rotated your Surface, use this to match the button orientation. +- **Button rotation**: If you've physically rotated your Surface, use this to match the button orientation. - **Never pin code lock**: Exclude this device from pin-code locking. -In the settings for each **Emulator**, you can configure additional parameters such as the row and column count here. +In the settings for each **Emulator**, you can configure additional behaviours. diff --git a/docs/3_config/surfaces/groups.md b/docs/3_config/surfaces/groups.md index ce2b5cb2b6..46a03646be 100644 --- a/docs/3_config/surfaces/groups.md +++ b/docs/3_config/surfaces/groups.md @@ -1,32 +1,10 @@ -Surface groups, introduced in Companion v3.2, allow multiple surfaces to be linked for several uses. - -## Use Case 1: Linking Control Surfaces - -All surfaces in a group are linked to the same page in Companion. This is convenient for setups such as having two Stream Deck XLs side by side, which want to be treated as a single 4x16 device. +Surface groups is a recent addition which allows for linking multiple surfaces to always be on the same page. +This is convenient for setups such as having two Stream Deck XLs side by side, which want to be treated as a single 16x4 device. To do this, use the **+ Add Group** button to create a new surface group. Then for each surface you want to add to the group, open its settings and change the surface group dropdown to the newly created group. -Finally, set the horizontal or vertical offset for each surface, to display the desired part of the Companion page. - -For example, in the _Settings > Buttons_ page specify the grid size to be 4 x 16, then on the _Surfaces_ page, put the two surfaces in a group, as described above, then select the line for the second Stream Deck XL in the group, and set _Horizontal Offset in grid_ to 8: the first XL will display columns 0-7; the second XL will show columns 8-15 of your pages. -Elsewhere in Companion, the actions that can change the page of a surface will no longer see the individual surfaces, and can instead pick the group. +Elsewhere in Companion, the actions which can change the page of a surface will no longer see the individual surfaces, and can instead pick the group. -## Use Case 2: Addressing a surface without being tied to its serial number - -A second use case for groups, is that you can use a group as a stable id for a surface. +Another use case for this, is that you can use a group as a stable id for a surface. This helps setups where you need to be able to swap out a Stream Deck quickly, and also need actions to reference specific Stream Decks. You can create your groups and program the system ahead of time, then later add or remove surfaces to the groups without needing to update any buttons. - -## Use Case 3: Establishing "Permission Groups" with restricted access - -Starting with v4.2, groups -- as well as ungrouped surfaces -- now provide the option to restrict which pages a device in that group may access. Using this in a surface-group configuration can be very useful if: - -1. Each surface is controlled independently (for example, each surface is controlled by its own computer or Raspberry Pi) - -...OR... - -2. It is expected that all user in a group should be on the same page (for example, if users are restricted to a single page) - -By creating groups for each role, a single site configuration can be created, with page access permissions determined by the device or group ID. Any surface "registered" to (i.e. included in) a particular group will then be able to access only the pages allowed for that surface group. - -See: [Page Permissions](#3_config/surfaces/pagepermissions.md) for additional details. diff --git a/docs/3_config/surfaces/images/restrict_pages.png b/docs/3_config/surfaces/images/restrict_pages.png deleted file mode 100644 index dc18b7718fa96ada6c37e127036d8b7ed22d4280..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6961 zcmb`MS5#A9m%t;UAWAP5n!=wVAVs7~FQO1S0qG!3Kmm~d(U2HowN68d+*=5iADxG^mMoA0002}lgC;n z0031SrR+s}nevu*@)4taF8G<~XaHb?T;nVd`N4W3N_|UVq^TU~emvxvIK`3yZ#ccRFYUlFd=1y?YK=oCs3ltO8Zl|QC zt}9r_pRrOT02N&{6XkPzo{9EvEfvH6gSgKacX5D?F8+Rbi>*6S{q8@NmPL(eA&(CD zggFyAi0C+?zhE1fqHV=tguT;Q$9cbaC~n|GoShNZx~=~bHA^mLWXv={?Fs-OX&Fz` z>BA{ta#uI8s!{!(E<^cPKXq9`!T~mjZ%hC^W2pZ-#F(L~qqHd2B=gqU*`Uowx5z;+6^eCiq zuz8TEcnK^RO2UZbq1E9(f)3L>@mv#@`UD=d6%NS}uId_!N~$K^T#s*Wo5)`Y{(&2R z?0d}jWJ|QCVVsn4T(qJhL|K^!uYTWs%n>zF-q6vTB4{I%bz9@O|$^h{LkzJD$? zeZ3N{+#9hyFXzi83~X}n^>|S1<^1;kMh;(7{!kb3k5XhXRhAZe89T)SKH&}p19ZO`y?1b(fCM_*thnKPbIUcTN(^u$` zDgzzH8`;wZc=>C8h88+hDimL`%MhmZrxwQJ&d(TzUn0+;UW?B)?mr5Z|7s>@j8YLd zT(ts8H#fWWp3NB7tLdRwOrs5FIlV_kw;!)1CnCKzZUrOH{mpB(CN_h0jS5;GD|RYK zZTihS5K9?!Ru%TU(enDCiz30naOJZ){Leq+HvNwA2(o?_;{9YxAgRSxmG!$C&=yLN zE@_n8iO&qPKR`Sn&fKg8H!k7&+uZgFFH!3O08xNEA6|`{4b{3i4xo|Oc-tSg_fo<} zsqdLO_L~drKd(gJTCRpzcCEIZu(@6@FRKq}X!+1zz3e{X4^pcMO*28#+K<>06ibE8 zRUcuv277(X;e22FsDYU3d*g0ZZ<-4hXFy+V&>bIDu`PXguC22`8|BPif3q{l_{(u3 zbH$@@J4;7a^%n?+squ)VYnSU<+02Z$Sd=%rTGCTQUY}KyQeh1m9-!1s?t7lN+3*|# z6>|8k)6T+!0z-?D1q`^P3$e=P#?rg!v|oG76(&yE_p%Fg#q7IUL%wo7%}_I?zMmN& zcLY>xi%}o;%qe{6A=HcS8-We`pigbYteSm6WKa3!Nc~Qq8TZU0n>6n0A@K{u z+0`Ul4Z80J5}bAqf5h8Hy)}Q^1B3*+0Q(4B8hWqiQ+8^>gs7)D+9mTcG_Wwt#TAeq zj^6W!sDf`QJp*Y>{!*#z{$MQN2NK*0eMNvm?eqy7%tz*!TxoeI z$s&`^D6NArtv$}ND+8o}KY0IZj|6?)%33!bo4uT!S-G z#x3A&kXPOXIK`ek(svF{`mqwK|p+C4=IoBo~pi zH{1r*lCm(#djE$F{x$`hH|UW8kvfSa5k;tf(UK%AC4a-7r%B=tk?e=);!F+*-BIZE zo5zQWB$c1^{eD6uFSC@*iiKbLJku;1v@mYhTkl&hV;yPG&bU**x)>pzu|tN?d9V5v zMsRh{o@&GmGL$9pP`jbQhWO31H?YuyIpKrF^p$RWTgrr#iV(N}exaBNaJ$LXmgyi2 z;2WO?HRrmq@BN{_hsVD^R_Ya_^M&~hE}FV$) z1;!!j=b`!aPhhzLJpgbWT3ain>{fskcJh&_*>}#jkf!kkH2Iz*4I@+`_Uve|99f#I z?HxES`6GziyKDMZ*u6DVS^yJ}Hcq=DUr3KQt>_@Q$lQ*Ke5Ny8kcg`}^Mt&ZRYSmS zKIJ&~PVY9fF)n#7cy_Ki0ix!w1Z7HnJD7)uFT8K>VThAajYxx?PR*@b94inq&x;q} zbD}uW)xomO_SRI`@wnzbWr1wMhMcb%2^x@>drA^IM9ssD54pwIcDhLS+XdTY0RUPK zYK?Cg4>pqDJ@GT`ljaHK06%DSyCE=FNxp(ARoN?Gd5g28resWUX z`S#$*Onz>TDHFw*-u?t$xAWimZ`570AMsriP+^G+4dysV|Iz2*Q-O0iiElN zbf(qtAp7qV-4m3YrQ?2|+7Hx8YNVzPHz8$Bn`hj6|M`C?1ay!%1xW_wD|!+GYEz_LOXd+At#8r!-+hBb6;jB4ez51b$`m0dH~@ETKQQ zCq-~6+LbWnW-Z}qFZFv%ca6^FZ9Yv<&FAKj97Wd|@$i3k6f_2&d3Kp84){_BM;-DV#n~*#Bb;)h1z=R{>{%~qeX^_O)nc&S_f4S7yFHoRdddNn zWOp~ZlAy(UBi{L84e_cWSKGLy^-8z8*ob4!KbhRAW82onY`E<&>31*Md{n;rswi2PYIbP~l zZrORUx*fVTtlR75k;+)c1Orn>Gvm#VI=!;q^?unAJ$uzr%W_+Y;n}Z(W#P@v-Cn6!t}=7y(a(ay}mteSH7n&Wjj0yJ>C#_!(W_;zNtFX zQxrFcmxs)0K5W>VHtZgl5(sH9((|bKl86_;Cg7S^zYJU+$Ny_ZxkaKkT-@IWd}2tpwR*rhD1V1GT(m?$3?T zA=l`kUL)}CZXT7(3Ei{(#+$6N=Xu^L7zH@0cr*sjO_hS5# zIyy%tbWDoTBX7#VwNQ^*BP^2dZ}-6O-@hMxtXU4H#jhTVt8Z@F-;({TOGBwL(pur9 z4gUd$sP`~X#7~v3z)UGh0RWus_qhKTKB6+zZd2kQfL5Mb~5(XRvFOHC#?sl{>=6s_N*{M^oAc0j>~l zYPnDVhpq$6d3r3xeUX{@-#JI}=Z#N4_|jRtn*Aketm<_o`nI~Hc}LbqE7nhg!(;}` zs&Z}BEo^)**#=--LK0)#>~CR;Q~}OYt$`9=s!d5ObHi850grB8%(_FJ0X&VY>iPKn z``{>P2PLtJ-Tgy8R_^Kp{%w)P_&0LAQ8T%P(-z+AL`ZO>aEy{O47@7UIKcl2x&)bM zS;x5Oj2{~eEM{AmXxBievk$|SkrY;{fK>#mNl*z}CsayX>G+S2-TkoaQ<5&b8(`a( z9cT|G4;((*P^!#nPBEFIu#4YAA6xCbn>!l!=z%#8;0=tBkEo8X9i9y--Ebs}drq@_N6_q5AzfD|h1T)+J9y z>0w*de(Rz5$T>ez44x(`Lqv&*p<5W{_N2dusTxoYTs!00NGF<}e_&ty9WnKRKa1-G zKJ&l^@g^icXH$R*wMqtCYRa^s_@h$Wi#|Cjqt0c}zAo=POJ+)mIi`#?RpXoVeqluc zvPNZgf7mc3<3y}Ie@55K5VJQKx*n6w+2&8-_`t(II{Q#Qo`@sIn`%KG?!iyRNo$|e z56m;=Ld3MJ3CKRrxyE~*L7!vMBijqNtnk3A7(=lACQdM54mHC(#UR+-T72#NG)jmK z`lZQhj&Ac*$qQ);Z^&K3AF#=YjU;oAu+`4;&PE<3=!;O#PVw!ypC9_333cgI6{)H< zEBed>wy@2aX}qei9On>RVc@bX9Cg-WXL3e&EzW%d)8{J*9Deh~h9weX%}*V*_|$-& zxc1iHGftjmOqJxYH0`7N!$gO4nc$rxeoqvp4+JixU7*@-jLL#PeF%V`)^oZe>0wWI99J8r|sRuW(dWQ6?uR4DCY{9fCeUR zpRuc!N5qVgYQaVKB;Vz19AC^5WeCCqa7WieViG4 zGL%g92y@lfYmL^DT^?|z3n8HfatfPOFrCQ9F0$)Z)A?Q$+!p_(;_*t(=^+}_`0M53 zu3LWBW~JLddZR~b2!AE%-~zC|bh8ysseJaEVtP?^D04sAGE*@ySO%%tx1MCu@T~$8 zF2i;l_JMJkl}5N@-%AZ$-)`L*_e}Oa$UO432Q9 zl9HIgj|%*;bGu(oT6;{YvTVL@iZ?H_oNQR2AQM@r7%In;>>GVblSYQ`H-4K*usS*p zeCK+>$GtDy;QUbASr}3>CEzhUBzexCU9Rkl^xCKGlb3s~Wv40MV)bI6LquZ>*40yG zOe4k+JLo6y@RstlYi8+Tb}FpgcQdz3)8oBnSRwb}(b+B1m{Ky_k4NWSG#ZhzgVO%4 z;$eq@6-h-o5H3u&gUw&8S5yFdhAh}Y!J6H>FFh8?yh8(hEsMBo@5*2048N#uCKU&t zK=PLGS!D{-2Z5d^clrbeIc%k}AR3FU6}17%NB`Vd}GW9m)ro= zZ^YTsf=qt|jDdLn#^z6NS48dVdhgmc-YpGnx?OG`!PwaMsrk(|q9{@T@}{P&LUuc7R(4ibU$S5yoQ_+256{nk`iOHmZEcl%F{vga1G%rt%Em;y<-=OelLEs4KW`2FWRDQ;RF%=6m|6ozLF*)w?D8q_nrDZ@R*Y?%ZN2 z7Tv4!sDqtn9NBw~f~ByJt^`+8*YB10-PmG2|B<&z-zbR1{fzYwhc{H|Rw|V@ZPt@d zo~GdwX#=%ZwZNp!0PrxwZ{*31lYFIYb5efmTW~RCe-K8S!Rh zx`!sRI94)@bd;Br&A`UR5nKz!OuEAEAQ%hgmD&+IC0+Zj$Oye`Mf<^yo5zVg&kj1T zN(>Fi%>jRXxc3}6;pwkeC3&p zwLf(1pI3@F^juOCzqnOsjX1=Mnf=R;ErLme&52(k^B9EN`g#TU!)PyWB+zfQzc`gs zZX{K1wz9G4CgrNn+)v5^X@)NYh5u^A1Zl-7DzKKFv@WMGm6VBChy?zIMm><%a=&?` zb(B1wbm>VFGUHRRx)dj@;`ieDiy9AZi%DlZ1YuR#S<22MfNrj%8#Pf8fWic%e_;n^ ziZ)8;$a$n;li1ZfnGGj-XgnoBgNRFFn2k3J2l|+7ZiWNM%a{WZ8nw5o3b%hqI(8Pl zl(jP}%g;tocVEG8(&QObNm>E6!3d|`%&$HR8X*V60UK*Uz(d>6)4g72o$V5-Y;_$M znd_r+5<)W*qc=-IlhOm6 zJ0v4ks(vfOiV*GclwCubV45j&D)O==d^Aeh0t)S#(9^DUNf;^#b`+{}>P1CJ`-E8L+ zxp7T%rTKmz(WBRgHZ0Z=D30TDh7j|3BYh8tB&RQlOIDtq5Xs7a-kAjON@1!uxe1%^ z0)>ZTIkc9IE^<9EMM{IzUqY7Q5KN;!(Rc~2&io4*%l$E&Hk?!|@Vvm%JKwIxrvz5E zdPbu2NGg07D84m0ys}YX${%xIqV;sYIi%0I>~4=`Qf%IDLqlNu1h|>s`Vs<`cid1d|=$jSGEY{%Ld-%_QxUy90Qd zTm^aBfY+~w4!5RV<~s5`{-T3hFTbbDmn){z(7}y@c}okDvP@-6EI%<&I##UTbCvtg z9jHs8IhIdhSA&LcJ9$DNrUYr272+?WzreJ@;Ck2`L|r#i&ADYTG*Y-xuK+7h_|TNI z9gmU$DE^6*hMi4z4sWoqsly=b@@yvm#a&lqiS#sdk;tM8`)e1N-(I z*p-d?WPa#~{-zA>=Ni^jru@nJ{@+YL_6QNM;$Dn|!-s$#h#gLlds zlbp^A&t$x!clqw)sTO)JMKi@vMGu(|O$>fJ!p0i>fq%Nv_)_Br1$UzA;?(3~Yb?d$3Qt`Z2YR-5?taA9QeXn0dm)<1A%7397E@&*^$@SHgvm*p`)kk2 zc%+Zwg17Lu2L*E8JTZwhjB3KG53J*uQSjJr2qKPnO-R}$^bBC6@-HZ$<94|Q05~{g z{U?cC{U7iWuG5oPkT|T_z1MdU0C4X5ixwSV{<1e}v_{TywtBBm`$4Z4sUxmZh-f+e z--x6H%zuKJo){gLn=5hZO)EC}-$>(_CH0N%r~@!8F)Occ-+t!Ra#$DW&QUnwNf{jW zzi Advanced (Admin UI Password)_: If enabled, Companion will require a password to view any of the configuration pages -2. [Surface Lockscreen](#3_config/settings/pin_lockout.md), _Settings > Surfaces (PIN Lockout)_: Allows all connected surfaces to be locked out after a timeout and requires a PIN to unlock. -3. [Page Permissions](#3_config/surfaces/pagepermissions.md) _Surfaces_ tab: If enabled for a device or a group, it allows you to limit page access for that device/group. -4. [Remote Access](#5_remote_control.md) _Settings > Protocols_: Determine whether remote computers/devices can initiate a connection to Companion. diff --git a/docs/structure.json b/docs/structure.json index 5792dfaf6f..4bb1a7f85c 100644 --- a/docs/structure.json +++ b/docs/structure.json @@ -90,10 +90,6 @@ { "label": "Discover", "file": "3_config/surfaces/discover.md" - }, - { - "label": "Page Permissions", - "file": "3_config/surfaces/pagepermissions.md" } ] }, @@ -165,11 +161,6 @@ } ] }, - { - "label": "Security Considerations", - "file": "security.md", - "children": [] - }, { "label": "Import / Export", "file": "3_config/import_export.md", diff --git a/shared-lib/lib/Model/Surfaces.ts b/shared-lib/lib/Model/Surfaces.ts index e4ac4b2b06..6e97433f23 100644 --- a/shared-lib/lib/Model/Surfaces.ts +++ b/shared-lib/lib/Model/Surfaces.ts @@ -65,15 +65,11 @@ export interface SurfaceGroupConfig { last_page_id: string startup_page_id: string use_last_page: boolean - restrict_pages?: boolean - allowed_page_ids?: string[] - /** @deprecated. replaced by last_page_id but still used for export */ + /** @deprecated. replaced by last_page_id */ last_page?: number - /** @deprecated. replaced by startup_page_id but still used for export */ + /** @deprecated. replaced by startup_page_id */ startup_page?: number - /** @deprecated. used for export */ - allowed_pages?: number[] } export interface SurfacePanelConfig { diff --git a/webui/src/Controls/InternalModuleField.tsx b/webui/src/Controls/InternalModuleField.tsx index ba59e20bcc..0f00d27540 100644 --- a/webui/src/Controls/InternalModuleField.tsx +++ b/webui/src/Controls/InternalModuleField.tsx @@ -185,7 +185,6 @@ interface InternalPageIdDropdownProps { value: any setValue: (value: any) => void disabled: boolean - multiple?: boolean } export const InternalPageIdDropdown = observer(function InternalPageDropdown({ @@ -195,7 +194,6 @@ export const InternalPageIdDropdown = observer(function InternalPageDropdown({ value, setValue, disabled, - multiple, }: InternalPageIdDropdownProps) { const { pages } = useContext(RootAppStoreContext) @@ -218,11 +216,7 @@ export const InternalPageIdDropdown = observer(function InternalPageDropdown({ return choices }, [pages, /*isLocatedInGrid,*/ includeStartup, includeDirection]) - if (multiple === undefined || !multiple) { - return - } else { - return - } + return }) interface InternalCustomVariableDropdownProps { diff --git a/webui/src/Surfaces/EditPanel.tsx b/webui/src/Surfaces/EditPanel.tsx index 76b24f95d6..ea0fc40f24 100644 --- a/webui/src/Surfaces/EditPanel.tsx +++ b/webui/src/Surfaces/EditPanel.tsx @@ -355,10 +355,10 @@ const SurfaceEditPanelContent = observer(function Surf /> - {(surfaceInfo === null || !!surfaceInfo.isConnected || !!groupConfig.config.use_last_page) && ( + {(surfaceId === null || !!surfaceInfo?.isConnected || !!groupConfig.config.use_last_page) && ( <> - {surfaceInfo === null || surfaceInfo?.isConnected ? 'Current Page' : 'Last Page'} + {surfaceId === null || surfaceInfo?.isConnected ? 'Current Page' : 'Last Page'} (function Surf )} - - - Restrict pages accessible to this {surfaceId === null ? 'group' : 'surface'} - - - setGroupConfigValue('restrict_pages', !!e.currentTarget.checked)} - /> - - - {!!groupConfig.config.restrict_pages && ( - <> - - Allowed pages: - - - setGroupConfigValue('allowed_page_ids', val)} - /> - - - )} )} From 4ad252163e44892bca3dd2014569e37444d11d4e Mon Sep 17 00:00:00 2001 From: arikorn Date: Wed, 29 Oct 2025 14:26:03 -0700 Subject: [PATCH 12/12] Change order of panel preferences. --- companion/lib/Surface/USB/ElgatoStreamDeck.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index 2be0aa43b7..ad8d7abd8f 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -38,9 +38,17 @@ export const StreamDeckJpegOptions: JPEGEncodeOptions = { function getConfigFields(streamDeck: StreamDeck): CompanionSurfaceConfigField[] { const fields: CompanionSurfaceConfigField[] = [...OffsetConfigFields] + fields.push(LegacyRotationConfigField) + + // Hide brightness for the pedal + const hasBrightness = !!streamDeck.CONTROLS.find( + (c) => c.type === 'lcd-segment' || (c.type === 'button' && c.feedbackType !== 'none') + ) + if (hasBrightness) fields.push(BrightnessConfigField) + if (streamDeck.MODEL === DeviceModelId.PLUS) { // place it above offset, etc. - fields.unshift({ + fields.push({ id: 'swipe_can_change_page', label: 'Horizontal Swipe Changes Page', type: 'checkbox', @@ -49,14 +57,7 @@ function getConfigFields(streamDeck: StreamDeck): CompanionSurfaceConfigField[] } as CompanionSurfaceConfigField) } - // Hide brightness for the pedal - const hasBrightness = !!streamDeck.CONTROLS.find( - (c) => c.type === 'lcd-segment' || (c.type === 'button' && c.feedbackType !== 'none') - ) - // is it safe/advisable to be pushing the object instead of a copy? - if (hasBrightness) fields.push(BrightnessConfigField) - - fields.push(LegacyRotationConfigField, ...LockConfigFields) + fields.push(...LockConfigFields) if (streamDeck.HAS_NFC_READER) fields.push({