mirror of
https://github.com/streamwall/streamwall.git
synced 2026-01-31 01:12:48 -05:00
Add view swap functionality
This commit is contained in:
@@ -63,6 +63,7 @@ The following hotkeys are available with the "control" webpage focused:
|
|||||||
|
|
||||||
- **alt+[1...9]**: Listen to the numbered stream
|
- **alt+[1...9]**: Listen to the numbered stream
|
||||||
- **alt+shift+[1...9]**: Toggle blur on the numbered stream
|
- **alt+shift+[1...9]**: Toggle blur on the numbered stream
|
||||||
|
- **alt+s**: Select the currently focused stream box to be swapped
|
||||||
- **alt+c**: Activate [Streamdelay](https://github.com/chromakode/streamdelay) censor mode
|
- **alt+c**: Activate [Streamdelay](https://github.com/chromakode/streamdelay) censor mode
|
||||||
- **alt+shift+c**: Deactivate [Streamdelay](https://github.com/chromakode/streamdelay) censor mode
|
- **alt+shift+c**: Deactivate [Streamdelay](https://github.com/chromakode/streamdelay) censor mode
|
||||||
|
|
||||||
|
|||||||
1
src/static/exchange-alt-solid.svg
Normal file
1
src/static/exchange-alt-solid.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="exchange-alt" class="svg-inline--fa fa-exchange-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M0 168v-16c0-13.255 10.745-24 24-24h360V80c0-21.367 25.899-32.042 40.971-16.971l80 80c9.372 9.373 9.372 24.569 0 33.941l-80 80C409.956 271.982 384 261.456 384 240v-48H24c-13.255 0-24-10.745-24-24zm488 152H128v-48c0-21.314-25.862-32.08-40.971-16.971l-80 80c-9.372 9.373-9.372 24.569 0 33.941l80 80C102.057 463.997 128 453.437 128 432v-48h360c13.255 0 24-10.745 24-24v-16c0-13.255-10.745-24-24-24z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 639 B |
@@ -21,6 +21,7 @@ import { idxInBox } from '../geometry'
|
|||||||
import SoundIcon from '../static/volume-up-solid.svg'
|
import SoundIcon from '../static/volume-up-solid.svg'
|
||||||
import NoVideoIcon from '../static/video-slash-solid.svg'
|
import NoVideoIcon from '../static/video-slash-solid.svg'
|
||||||
import ReloadIcon from '../static/redo-alt-solid.svg'
|
import ReloadIcon from '../static/redo-alt-solid.svg'
|
||||||
|
import SwapIcon from '../static/exchange-alt-solid.svg'
|
||||||
import LifeRingIcon from '../static/life-ring-regular.svg'
|
import LifeRingIcon from '../static/life-ring-regular.svg'
|
||||||
import WindowIcon from '../static/window-maximize-regular.svg'
|
import WindowIcon from '../static/window-maximize-regular.svg'
|
||||||
import { idColor } from './colors'
|
import { idColor } from './colors'
|
||||||
@@ -137,6 +138,7 @@ function useStreamwallConnection(wsEndpoint) {
|
|||||||
state,
|
state,
|
||||||
isListening,
|
isListening,
|
||||||
isBlurred,
|
isBlurred,
|
||||||
|
spaces: pos.spaces,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,26 +216,72 @@ function App({ wsEndpoint }) {
|
|||||||
setShowDebug(ev.target.checked)
|
setShowDebug(ev.target.checked)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [swapStartIdx, setSwapStartIdx] = useState()
|
||||||
|
const handleSwapView = useCallback(
|
||||||
|
(idx) => {
|
||||||
|
if (!stateIdxMap.has(idx)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Deselect the input so the contents aren't persisted by GridInput's `editingValue`
|
||||||
|
document.activeElement.blur()
|
||||||
|
setSwapStartIdx(idx)
|
||||||
|
},
|
||||||
|
[stateIdxMap],
|
||||||
|
)
|
||||||
|
const handleSwap = useCallback(
|
||||||
|
(toIdx) => {
|
||||||
|
if (swapStartIdx === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stateDoc.transact(() => {
|
||||||
|
const viewsState = stateDoc.getMap('views')
|
||||||
|
const startStreamId = viewsState
|
||||||
|
.get(String(swapStartIdx))
|
||||||
|
.get('streamId')
|
||||||
|
const toStreamId = viewsState.get(String(toIdx)).get('streamId')
|
||||||
|
const startSpaces = stateIdxMap.get(swapStartIdx).spaces
|
||||||
|
const toSpaces = stateIdxMap.get(toIdx).spaces
|
||||||
|
for (const startSpaceIdx of startSpaces) {
|
||||||
|
viewsState.get(String(startSpaceIdx)).set('streamId', toStreamId)
|
||||||
|
}
|
||||||
|
for (const toSpaceIdx of toSpaces) {
|
||||||
|
viewsState.get(String(toSpaceIdx)).set('streamId', startStreamId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setSwapStartIdx()
|
||||||
|
},
|
||||||
|
[stateDoc, stateIdxMap, swapStartIdx],
|
||||||
|
)
|
||||||
|
|
||||||
const [dragStart, setDragStart] = useState()
|
const [dragStart, setDragStart] = useState()
|
||||||
const handleDragStart = useCallback((idx, ev) => {
|
const handleDragStart = useCallback(
|
||||||
setDragStart(idx)
|
(idx, ev) => {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
}, [])
|
if (swapStartIdx !== undefined) {
|
||||||
|
handleSwap(idx)
|
||||||
|
} else {
|
||||||
|
setDragStart(idx)
|
||||||
|
ev.target.select()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleSwap],
|
||||||
|
)
|
||||||
const [dragEnd, setDragEnd] = useState()
|
const [dragEnd, setDragEnd] = useState()
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
function endDrag() {
|
function endDrag() {
|
||||||
if (dragStart !== undefined) {
|
if (dragStart === undefined) {
|
||||||
stateDoc.transact(() => {
|
return
|
||||||
const viewsState = stateDoc.getMap('views')
|
|
||||||
const streamId = viewsState.get(String(dragStart)).get('streamId')
|
|
||||||
for (let idx = 0; idx < gridCount ** 2; idx++) {
|
|
||||||
if (idxInBox(gridCount, dragStart, dragEnd, idx)) {
|
|
||||||
viewsState.get(String(idx)).set('streamId', streamId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setDragStart()
|
|
||||||
}
|
}
|
||||||
|
stateDoc.transact(() => {
|
||||||
|
const viewsState = stateDoc.getMap('views')
|
||||||
|
const streamId = viewsState.get(String(dragStart)).get('streamId')
|
||||||
|
for (let idx = 0; idx < gridCount ** 2; idx++) {
|
||||||
|
if (idxInBox(gridCount, dragStart, dragEnd, idx)) {
|
||||||
|
viewsState.get(String(idx)).set('streamId', streamId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setDragStart()
|
||||||
}
|
}
|
||||||
window.addEventListener('mouseup', endDrag)
|
window.addEventListener('mouseup', endDrag)
|
||||||
return () => window.removeEventListener('mouseup', endDrag)
|
return () => window.removeEventListener('mouseup', endDrag)
|
||||||
@@ -361,6 +409,8 @@ function App({ wsEndpoint }) {
|
|||||||
const isListening = stateIdxMap.get(idx)?.isListening ?? false
|
const isListening = stateIdxMap.get(idx)?.isListening ?? false
|
||||||
handleSetListening(idx, !isListening)
|
handleSetListening(idx, !isListening)
|
||||||
},
|
},
|
||||||
|
// This enables hotkeys when input elements are focused, and affects all hotkeys, not just this one.
|
||||||
|
{ filter: () => true },
|
||||||
[stateIdxMap],
|
[stateIdxMap],
|
||||||
)
|
)
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
@@ -387,6 +437,13 @@ function App({ wsEndpoint }) {
|
|||||||
},
|
},
|
||||||
[setStreamCensored],
|
[setStreamCensored],
|
||||||
)
|
)
|
||||||
|
useHotkeys(
|
||||||
|
`alt+s`,
|
||||||
|
() => {
|
||||||
|
handleSwapView(focusedInputIdx)
|
||||||
|
},
|
||||||
|
[handleSwapView, focusedInputIdx],
|
||||||
|
)
|
||||||
|
|
||||||
const normalStreams = streams.filter(
|
const normalStreams = streams.filter(
|
||||||
(s) => !s.kind || s.kind === 'video' || s.kind === 'web',
|
(s) => !s.kind || s.kind === 'video' || s.kind === 'web',
|
||||||
@@ -426,6 +483,7 @@ function App({ wsEndpoint }) {
|
|||||||
isListening={isListening}
|
isListening={isListening}
|
||||||
isBlurred={isBlurred}
|
isBlurred={isBlurred}
|
||||||
isHighlighted={isDragHighlighted}
|
isHighlighted={isDragHighlighted}
|
||||||
|
isSwapping={idx === swapStartIdx}
|
||||||
showDebug={showDebug}
|
showDebug={showDebug}
|
||||||
onMouseDown={handleDragStart}
|
onMouseDown={handleDragStart}
|
||||||
onMouseEnter={setDragEnd}
|
onMouseEnter={setDragEnd}
|
||||||
@@ -435,6 +493,7 @@ function App({ wsEndpoint }) {
|
|||||||
onSetListening={handleSetListening}
|
onSetListening={handleSetListening}
|
||||||
onSetBlurred={handleSetBlurred}
|
onSetBlurred={handleSetBlurred}
|
||||||
onReloadView={handleReloadView}
|
onReloadView={handleReloadView}
|
||||||
|
onSwapView={handleSwapView}
|
||||||
onBrowse={handleBrowse}
|
onBrowse={handleBrowse}
|
||||||
onDevTools={handleDevTools}
|
onDevTools={handleDevTools}
|
||||||
/>
|
/>
|
||||||
@@ -579,6 +638,7 @@ function GridInput({
|
|||||||
isListening,
|
isListening,
|
||||||
isBlurred,
|
isBlurred,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
|
isSwapping,
|
||||||
showDebug,
|
showDebug,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
@@ -587,6 +647,7 @@ function GridInput({
|
|||||||
onSetListening,
|
onSetListening,
|
||||||
onSetBlurred,
|
onSetBlurred,
|
||||||
onReloadView,
|
onReloadView,
|
||||||
|
onSwapView,
|
||||||
onBrowse,
|
onBrowse,
|
||||||
onDevTools,
|
onDevTools,
|
||||||
}) {
|
}) {
|
||||||
@@ -626,6 +687,7 @@ function GridInput({
|
|||||||
idx,
|
idx,
|
||||||
onReloadView,
|
onReloadView,
|
||||||
])
|
])
|
||||||
|
const handleSwapClick = useCallback(() => onSwapView(idx), [idx, onSwapView])
|
||||||
const handleBrowseClick = useCallback(() => onBrowse(spaceValue), [
|
const handleBrowseClick = useCallback(() => onBrowse(spaceValue), [
|
||||||
spaceValue,
|
spaceValue,
|
||||||
onBrowse,
|
onBrowse,
|
||||||
@@ -636,7 +698,6 @@ function GridInput({
|
|||||||
])
|
])
|
||||||
const handleMouseDown = useCallback(
|
const handleMouseDown = useCallback(
|
||||||
(ev) => {
|
(ev) => {
|
||||||
ev.target.select()
|
|
||||||
onMouseDown(idx, ev)
|
onMouseDown(idx, ev)
|
||||||
},
|
},
|
||||||
[onMouseDown],
|
[onMouseDown],
|
||||||
@@ -646,10 +707,7 @@ function GridInput({
|
|||||||
<StyledGridContainer>
|
<StyledGridContainer>
|
||||||
{isDisplaying && (
|
{isDisplaying && (
|
||||||
<StyledGridButtons side="left">
|
<StyledGridButtons side="left">
|
||||||
<StyledSmallButton onClick={handleReloadClick} tabIndex={1}>
|
{showDebug ? (
|
||||||
<ReloadIcon />
|
|
||||||
</StyledSmallButton>
|
|
||||||
{showDebug && (
|
|
||||||
<>
|
<>
|
||||||
<StyledSmallButton onClick={handleBrowseClick} tabIndex={1}>
|
<StyledSmallButton onClick={handleBrowseClick} tabIndex={1}>
|
||||||
<WindowIcon />
|
<WindowIcon />
|
||||||
@@ -658,6 +716,19 @@ function GridInput({
|
|||||||
<LifeRingIcon />
|
<LifeRingIcon />
|
||||||
</StyledSmallButton>
|
</StyledSmallButton>
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<StyledSmallButton onClick={handleReloadClick} tabIndex={1}>
|
||||||
|
<ReloadIcon />
|
||||||
|
</StyledSmallButton>
|
||||||
|
<StyledSmallButton
|
||||||
|
isActive={isSwapping}
|
||||||
|
onClick={handleSwapClick}
|
||||||
|
tabIndex={1}
|
||||||
|
>
|
||||||
|
<SwapIcon />
|
||||||
|
</StyledSmallButton>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</StyledGridButtons>
|
</StyledGridButtons>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user