mirror of
https://github.com/streamwall/streamwall.git
synced 2026-01-24 22:22:50 -05:00
Add view swap functionality
This commit is contained in:
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 NoVideoIcon from '../static/video-slash-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 WindowIcon from '../static/window-maximize-regular.svg'
|
||||
import { idColor } from './colors'
|
||||
@@ -137,6 +138,7 @@ function useStreamwallConnection(wsEndpoint) {
|
||||
state,
|
||||
isListening,
|
||||
isBlurred,
|
||||
spaces: pos.spaces,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -214,26 +216,72 @@ function App({ wsEndpoint }) {
|
||||
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 handleDragStart = useCallback((idx, ev) => {
|
||||
setDragStart(idx)
|
||||
ev.preventDefault()
|
||||
}, [])
|
||||
const handleDragStart = useCallback(
|
||||
(idx, ev) => {
|
||||
ev.preventDefault()
|
||||
if (swapStartIdx !== undefined) {
|
||||
handleSwap(idx)
|
||||
} else {
|
||||
setDragStart(idx)
|
||||
ev.target.select()
|
||||
}
|
||||
},
|
||||
[handleSwap],
|
||||
)
|
||||
const [dragEnd, setDragEnd] = useState()
|
||||
useLayoutEffect(() => {
|
||||
function endDrag() {
|
||||
if (dragStart !== undefined) {
|
||||
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()
|
||||
if (dragStart === undefined) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
return () => window.removeEventListener('mouseup', endDrag)
|
||||
@@ -361,6 +409,8 @@ function App({ wsEndpoint }) {
|
||||
const isListening = stateIdxMap.get(idx)?.isListening ?? false
|
||||
handleSetListening(idx, !isListening)
|
||||
},
|
||||
// This enables hotkeys when input elements are focused, and affects all hotkeys, not just this one.
|
||||
{ filter: () => true },
|
||||
[stateIdxMap],
|
||||
)
|
||||
useHotkeys(
|
||||
@@ -387,6 +437,13 @@ function App({ wsEndpoint }) {
|
||||
},
|
||||
[setStreamCensored],
|
||||
)
|
||||
useHotkeys(
|
||||
`alt+s`,
|
||||
() => {
|
||||
handleSwapView(focusedInputIdx)
|
||||
},
|
||||
[handleSwapView, focusedInputIdx],
|
||||
)
|
||||
|
||||
const normalStreams = streams.filter(
|
||||
(s) => !s.kind || s.kind === 'video' || s.kind === 'web',
|
||||
@@ -426,6 +483,7 @@ function App({ wsEndpoint }) {
|
||||
isListening={isListening}
|
||||
isBlurred={isBlurred}
|
||||
isHighlighted={isDragHighlighted}
|
||||
isSwapping={idx === swapStartIdx}
|
||||
showDebug={showDebug}
|
||||
onMouseDown={handleDragStart}
|
||||
onMouseEnter={setDragEnd}
|
||||
@@ -435,6 +493,7 @@ function App({ wsEndpoint }) {
|
||||
onSetListening={handleSetListening}
|
||||
onSetBlurred={handleSetBlurred}
|
||||
onReloadView={handleReloadView}
|
||||
onSwapView={handleSwapView}
|
||||
onBrowse={handleBrowse}
|
||||
onDevTools={handleDevTools}
|
||||
/>
|
||||
@@ -579,6 +638,7 @@ function GridInput({
|
||||
isListening,
|
||||
isBlurred,
|
||||
isHighlighted,
|
||||
isSwapping,
|
||||
showDebug,
|
||||
onMouseDown,
|
||||
onMouseEnter,
|
||||
@@ -587,6 +647,7 @@ function GridInput({
|
||||
onSetListening,
|
||||
onSetBlurred,
|
||||
onReloadView,
|
||||
onSwapView,
|
||||
onBrowse,
|
||||
onDevTools,
|
||||
}) {
|
||||
@@ -626,6 +687,7 @@ function GridInput({
|
||||
idx,
|
||||
onReloadView,
|
||||
])
|
||||
const handleSwapClick = useCallback(() => onSwapView(idx), [idx, onSwapView])
|
||||
const handleBrowseClick = useCallback(() => onBrowse(spaceValue), [
|
||||
spaceValue,
|
||||
onBrowse,
|
||||
@@ -636,7 +698,6 @@ function GridInput({
|
||||
])
|
||||
const handleMouseDown = useCallback(
|
||||
(ev) => {
|
||||
ev.target.select()
|
||||
onMouseDown(idx, ev)
|
||||
},
|
||||
[onMouseDown],
|
||||
@@ -646,10 +707,7 @@ function GridInput({
|
||||
<StyledGridContainer>
|
||||
{isDisplaying && (
|
||||
<StyledGridButtons side="left">
|
||||
<StyledSmallButton onClick={handleReloadClick} tabIndex={1}>
|
||||
<ReloadIcon />
|
||||
</StyledSmallButton>
|
||||
{showDebug && (
|
||||
{showDebug ? (
|
||||
<>
|
||||
<StyledSmallButton onClick={handleBrowseClick} tabIndex={1}>
|
||||
<WindowIcon />
|
||||
@@ -658,6 +716,19 @@ function GridInput({
|
||||
<LifeRingIcon />
|
||||
</StyledSmallButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StyledSmallButton onClick={handleReloadClick} tabIndex={1}>
|
||||
<ReloadIcon />
|
||||
</StyledSmallButton>
|
||||
<StyledSmallButton
|
||||
isActive={isSwapping}
|
||||
onClick={handleSwapClick}
|
||||
tabIndex={1}
|
||||
>
|
||||
<SwapIcon />
|
||||
</StyledSmallButton>
|
||||
</>
|
||||
)}
|
||||
</StyledGridButtons>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user