mirror of
https://github.com/streamwall/streamwall.git
synced 2026-04-03 20:32:08 -04:00
Add drag interaction to duplicate stream ids in control page
This commit is contained in:
@@ -5,7 +5,7 @@ import { BrowserView, BrowserWindow, ipcMain } from 'electron'
|
|||||||
import { interpret } from 'xstate'
|
import { interpret } from 'xstate'
|
||||||
|
|
||||||
import viewStateMachine from './viewStateMachine'
|
import viewStateMachine from './viewStateMachine'
|
||||||
import { boxesFromViewContentMap } from './geometry'
|
import { boxesFromViewContentMap } from '../geometry'
|
||||||
|
|
||||||
export default class StreamWindow extends EventEmitter {
|
export default class StreamWindow extends EventEmitter {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import isEqual from 'lodash/isEqual'
|
|
||||||
|
|
||||||
export function boxesFromViewContentMap(width, height, viewContentMap) {
|
|
||||||
const boxes = []
|
|
||||||
const visited = new Set()
|
|
||||||
|
|
||||||
function isPosContent(x, y, content) {
|
|
||||||
const checkIdx = width * y + x
|
|
||||||
return (
|
|
||||||
!visited.has(checkIdx) && isEqual(viewContentMap.get(checkIdx), content)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function findLargestBox(x, y) {
|
|
||||||
const idx = width * y + x
|
|
||||||
const spaces = [idx]
|
|
||||||
const content = viewContentMap.get(idx)
|
|
||||||
|
|
||||||
let maxY
|
|
||||||
for (maxY = y + 1; maxY < height; maxY++) {
|
|
||||||
if (!isPosContent(x, maxY, content)) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
spaces.push(width * maxY + x)
|
|
||||||
}
|
|
||||||
|
|
||||||
let cx = x
|
|
||||||
let cy = y
|
|
||||||
scan: for (cx = x + 1; cx < width; cx++) {
|
|
||||||
for (cy = y; cy < maxY; cy++) {
|
|
||||||
if (!isPosContent(cx, cy, content)) {
|
|
||||||
break scan
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let cy = y; cy < maxY; cy++) {
|
|
||||||
spaces.push(width * cy + cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const w = cx - x
|
|
||||||
const h = maxY - y
|
|
||||||
spaces.sort()
|
|
||||||
return { content, x, y, w, h, spaces }
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let y = 0; y < width; y++) {
|
|
||||||
for (let x = 0; x < height; x++) {
|
|
||||||
const idx = width * y + x
|
|
||||||
if (visited.has(idx) || viewContentMap.get(idx) === undefined) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const box = findLargestBox(x, y)
|
|
||||||
boxes.push(box)
|
|
||||||
for (const boxIdx of box.spaces) {
|
|
||||||
visited.add(boxIdx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return boxes
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import { boxesFromViewContentMap } from './geometry'
|
|
||||||
|
|
||||||
function example([text]) {
|
|
||||||
return text
|
|
||||||
.replace(/\s/g, '')
|
|
||||||
.split('')
|
|
||||||
.map((c) => (c === '.' ? undefined : { url: c }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const box1 = example`
|
|
||||||
ab
|
|
||||||
ab
|
|
||||||
`
|
|
||||||
|
|
||||||
const box2 = example`
|
|
||||||
aa
|
|
||||||
bb
|
|
||||||
`
|
|
||||||
|
|
||||||
const box3 = example`
|
|
||||||
aac
|
|
||||||
aaa
|
|
||||||
dae
|
|
||||||
`
|
|
||||||
|
|
||||||
const box4 = example`
|
|
||||||
...
|
|
||||||
.aa
|
|
||||||
.aa
|
|
||||||
`
|
|
||||||
|
|
||||||
const box5 = example`
|
|
||||||
..a
|
|
||||||
..a
|
|
||||||
.aa
|
|
||||||
`
|
|
||||||
|
|
||||||
describe.each([
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
box1,
|
|
||||||
[
|
|
||||||
{ content: { url: 'a' }, x: 0, y: 0, w: 1, h: 2, spaces: [0, 2] },
|
|
||||||
{ content: { url: 'b' }, x: 1, y: 0, w: 1, h: 2, spaces: [1, 3] },
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
box2,
|
|
||||||
[
|
|
||||||
{ content: { url: 'a' }, x: 0, y: 0, w: 2, h: 1, spaces: [0, 1] },
|
|
||||||
{ content: { url: 'b' }, x: 0, y: 1, w: 2, h: 1, spaces: [2, 3] },
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
box3,
|
|
||||||
[
|
|
||||||
{ content: { url: 'a' }, x: 0, y: 0, w: 2, h: 2, spaces: [0, 1, 3, 4] },
|
|
||||||
{ content: { url: 'c' }, x: 2, y: 0, w: 1, h: 1, spaces: [2] },
|
|
||||||
{ content: { url: 'a' }, x: 2, y: 1, w: 1, h: 1, spaces: [5] },
|
|
||||||
{ content: { url: 'd' }, x: 0, y: 2, w: 1, h: 1, spaces: [6] },
|
|
||||||
{ content: { url: 'a' }, x: 1, y: 2, w: 1, h: 1, spaces: [7] },
|
|
||||||
{ content: { url: 'e' }, x: 2, y: 2, w: 1, h: 1, spaces: [8] },
|
|
||||||
],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
box4,
|
|
||||||
[{ content: { url: 'a' }, x: 1, y: 1, w: 2, h: 2, spaces: [4, 5, 7, 8] }],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
box5,
|
|
||||||
[
|
|
||||||
{ content: { url: 'a' }, x: 2, y: 0, w: 1, h: 3, spaces: [2, 5, 8] },
|
|
||||||
{ content: { url: 'a' }, x: 1, y: 2, w: 1, h: 1, spaces: [7] },
|
|
||||||
],
|
|
||||||
],
|
|
||||||
])('boxesFromViewContentMap(%i, %i, %j)', (width, height, data, expected) => {
|
|
||||||
test(`returns expected ${expected.length} boxes`, () => {
|
|
||||||
const stateURLMap = new Map(data.map((v, idx) => [idx, v]))
|
|
||||||
const result = boxesFromViewContentMap(width, height, stateURLMap)
|
|
||||||
expect(result).toStrictEqual(expected)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -11,6 +11,7 @@ import styled, { css } from 'styled-components'
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
import '../index.css'
|
import '../index.css'
|
||||||
|
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'
|
||||||
@@ -185,9 +186,33 @@ function App({ wsEndpoint }) {
|
|||||||
stateIdxMap,
|
stateIdxMap,
|
||||||
delayState,
|
delayState,
|
||||||
} = useStreamwallConnection(wsEndpoint)
|
} = useStreamwallConnection(wsEndpoint)
|
||||||
|
|
||||||
const { gridCount } = config
|
const { gridCount } = config
|
||||||
|
|
||||||
|
const [dragStart, setDragStart] = useState()
|
||||||
|
const handleDragStart = useCallback((idx, ev) => {
|
||||||
|
setDragStart(idx)
|
||||||
|
ev.preventDefault()
|
||||||
|
}, [])
|
||||||
|
const [dragEnd, setDragEnd] = useState()
|
||||||
|
useEffect(() => {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('mouseup', endDrag)
|
||||||
|
return () => window.removeEventListener('mouseup', endDrag)
|
||||||
|
}, [stateDoc, dragStart, dragEnd])
|
||||||
|
|
||||||
const handleSetView = useCallback(
|
const handleSetView = useCallback(
|
||||||
(idx, streamId) => {
|
(idx, streamId) => {
|
||||||
const stream = streams.find((d) => d._id === streamId)
|
const stream = streams.find((d) => d._id === streamId)
|
||||||
@@ -344,6 +369,9 @@ function App({ wsEndpoint }) {
|
|||||||
const { isListening = false, isBlurred = false, state } =
|
const { isListening = false, isBlurred = false, state } =
|
||||||
stateIdxMap.get(idx) || {}
|
stateIdxMap.get(idx) || {}
|
||||||
const { streamId } = sharedState.views?.[idx] || ''
|
const { streamId } = sharedState.views?.[idx] || ''
|
||||||
|
const isDragHighlighted =
|
||||||
|
dragStart !== undefined &&
|
||||||
|
idxInBox(gridCount, dragStart, dragEnd, idx)
|
||||||
return (
|
return (
|
||||||
<GridInput
|
<GridInput
|
||||||
idx={idx}
|
idx={idx}
|
||||||
@@ -352,6 +380,9 @@ function App({ wsEndpoint }) {
|
|||||||
isDisplaying={state && state.matches('displaying')}
|
isDisplaying={state && state.matches('displaying')}
|
||||||
isListening={isListening}
|
isListening={isListening}
|
||||||
isBlurred={isBlurred}
|
isBlurred={isBlurred}
|
||||||
|
isHighlighted={isDragHighlighted}
|
||||||
|
onMouseDown={handleDragStart}
|
||||||
|
onMouseEnter={setDragEnd}
|
||||||
onChangeSpace={handleSetView}
|
onChangeSpace={handleSetView}
|
||||||
onSetListening={handleSetListening}
|
onSetListening={handleSetListening}
|
||||||
onSetBlurred={handleSetBlurred}
|
onSetBlurred={handleSetBlurred}
|
||||||
@@ -472,6 +503,9 @@ function GridInput({
|
|||||||
isError,
|
isError,
|
||||||
isListening,
|
isListening,
|
||||||
isBlurred,
|
isBlurred,
|
||||||
|
isHighlighted,
|
||||||
|
onMouseDown,
|
||||||
|
onMouseEnter,
|
||||||
onSetListening,
|
onSetListening,
|
||||||
onSetBlurred,
|
onSetBlurred,
|
||||||
onReloadView,
|
onReloadView,
|
||||||
@@ -514,9 +548,14 @@ function GridInput({
|
|||||||
idx,
|
idx,
|
||||||
onDevTools,
|
onDevTools,
|
||||||
])
|
])
|
||||||
const handleClick = useCallback((ev) => {
|
const handleMouseDown = useCallback(
|
||||||
|
(ev) => {
|
||||||
ev.target.select()
|
ev.target.select()
|
||||||
})
|
onMouseDown(idx, ev)
|
||||||
|
},
|
||||||
|
[onMouseDown],
|
||||||
|
)
|
||||||
|
const handleMouseEnter = useCallback(() => onMouseEnter(idx), [onMouseEnter])
|
||||||
return (
|
return (
|
||||||
<StyledGridContainer>
|
<StyledGridContainer>
|
||||||
{isDisplaying && (
|
{isDisplaying && (
|
||||||
@@ -552,9 +591,11 @@ function GridInput({
|
|||||||
name={idx}
|
name={idx}
|
||||||
value={editingValue || spaceValue || ''}
|
value={editingValue || spaceValue || ''}
|
||||||
isError={isError}
|
isError={isError}
|
||||||
|
isHighlighted={isHighlighted}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onClick={handleClick}
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
</StyledGridContainer>
|
</StyledGridContainer>
|
||||||
@@ -675,6 +716,7 @@ const StyledGridInput = styled.input`
|
|||||||
height: 50px;
|
height: 50px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 2px solid ${({ isError }) => (isError ? 'red' : 'black')};
|
border: 2px solid ${({ isError }) => (isError ? 'red' : 'black')};
|
||||||
|
background: ${({ isHighlighted }) => (isHighlighted ? '#dfd' : 'white')};
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user