diff --git a/README.md b/README.md
index 3d01413..8a22ae6 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ The following hotkeys are available with the "control" webpage focused:
- **alt+[1...9]**: Listen to 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+shift+c**: Deactivate [Streamdelay](https://github.com/chromakode/streamdelay) censor mode
diff --git a/src/static/exchange-alt-solid.svg b/src/static/exchange-alt-solid.svg
new file mode 100644
index 0000000..03af661
--- /dev/null
+++ b/src/static/exchange-alt-solid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/web/control.js b/src/web/control.js
index a7a0d85..256fffa 100644
--- a/src/web/control.js
+++ b/src/web/control.js
@@ -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({
{isDisplaying && (
-
-
-
- {showDebug && (
+ {showDebug ? (
<>
@@ -658,6 +716,19 @@ function GridInput({
>
+ ) : (
+ <>
+
+
+
+
+
+
+ >
)}
)}