Skip to content

Commit

Permalink
feat: retime piece user action
Browse files Browse the repository at this point in the history
  • Loading branch information
mint-dewit committed Dec 18, 2024
1 parent f7a4fe7 commit 1284f9f
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 19 deletions.
12 changes: 12 additions & 0 deletions packages/blueprints-integration/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export enum DefaultUserOperationsTypes {
REVERT_PART = '__sofie-revert-part',
REVERT_RUNDOWN = '__sofie-revert-rundown',
UPDATE_PROPS = '__sofie-update-props',
RETIME_PIECE = '__sofie-retime-piece',
}

export interface DefaultUserOperationRevertRundown {
Expand All @@ -153,6 +154,17 @@ export interface DefaultUserOperationEditProperties {
}
}

export type DefaultUserOperationRetimePiece = {
id: DefaultUserOperationsTypes.RETIME_PIECE
payload: {
segmentExternalId: string
partExternalId: string

inPoint: number
// note - at some point this could also include an updated duration
}
}

export type DefaultUserOperations =
| DefaultUserOperationRevertRundown
| DefaultUserOperationRevertSegment
Expand Down
17 changes: 16 additions & 1 deletion packages/blueprints-integration/src/userEditing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import type { JSONBlob } from '@sofie-automation/shared-lib/dist/lib/JSONBlob'
import type { ITranslatableMessage } from './translations'
import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes'
import { SourceLayerType } from './content'
import { DefaultUserOperationsTypes } from './ingest'

/**
* Description of a user performed editing operation allowed on an document
*/
export type UserEditingDefinition = UserEditingDefinitionAction | UserEditingDefinitionForm
export type UserEditingDefinition =
| UserEditingDefinitionAction
| UserEditingDefinitionForm
| UserEditingDefinitionSofieDefault

/**
* A simple 'action' that can be performed
Expand Down Expand Up @@ -40,11 +44,22 @@ export interface UserEditingDefinitionForm {
currentValues: Record<string, any>
}

/**
* A built in Sofie User operation
*/
export interface UserEditingDefinitionSofieDefault {
type: UserEditingType.SOFIE
/** Id of this operation */
id: DefaultUserOperationsTypes
}

export enum UserEditingType {
/** Action */
ACTION = 'action',
/** Form */
FORM = 'form',
/** Operation for the Built-in Sofie Rich Editing UI */
SOFIE = 'sofie',
}

export interface UserEditingSourceLayer {
Expand Down
12 changes: 11 additions & 1 deletion packages/corelib/src/dataModel/UserEditingDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import type {
JSONBlob,
JSONSchema,
UserEditingSourceLayer,
DefaultUserOperationsTypes,
} from '@sofie-automation/blueprints-integration'
import type { ITranslatableMessage } from '../TranslatableMessage'

export type CoreUserEditingDefinition = CoreUserEditingDefinitionAction | CoreUserEditingDefinitionForm
export type CoreUserEditingDefinition =
| CoreUserEditingDefinitionAction
| CoreUserEditingDefinitionForm
| CoreUserEditingDefinitionSofie

export interface CoreUserEditingDefinitionAction {
type: UserEditingType.ACTION
Expand Down Expand Up @@ -83,3 +87,9 @@ export interface CoreUserEditingProperties {
/** Translation namespaces to use when rendering this form */
translationNamespaces: string[]
}

export interface CoreUserEditingDefinitionSofie {
type: UserEditingType.SOFIE
/** Id of this operation */
id: DefaultUserOperationsTypes
}
27 changes: 19 additions & 8 deletions packages/job-worker/src/blueprints/context/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import {
UserEditingDefinition,
UserEditingDefinitionAction,
UserEditingDefinitionForm,
UserEditingDefinitionSofieDefault,
UserEditingProperties,
UserEditingType,
} from '@sofie-automation/blueprints-integration/dist/userEditing'
Expand Down Expand Up @@ -516,22 +517,27 @@ function translateUserEditsToBlueprint(
userEdits.map((userEdit) => {
switch (userEdit.type) {
case UserEditingType.ACTION:
return {
return literal<UserEditingDefinitionAction>({
type: UserEditingType.ACTION,
id: userEdit.id,
label: omit(userEdit.label, 'namespaces'),
svgIcon: userEdit.svgIcon,
svgIconInactive: userEdit.svgIconInactive,
isActive: userEdit.isActive,
} satisfies Complete<UserEditingDefinitionAction>
})
case UserEditingType.FORM:
return {
return literal<UserEditingDefinitionForm>({
type: UserEditingType.FORM,
id: userEdit.id,
label: omit(userEdit.label, 'namespaces'),
schema: clone(userEdit.schema),
currentValues: clone(userEdit.currentValues),
} satisfies Complete<UserEditingDefinitionForm>
})
case UserEditingType.SOFIE:
return literal<UserEditingDefinitionSofieDefault>({
type: UserEditingType.SOFIE,
id: userEdit.id,
})
default:
assertNever(userEdit)
return undefined
Expand Down Expand Up @@ -573,23 +579,28 @@ export function translateUserEditsFromBlueprint(
userEdits.map((userEdit) => {
switch (userEdit.type) {
case UserEditingType.ACTION:
return {
return literal<CoreUserEditingDefinitionAction>({
type: UserEditingType.ACTION,
id: userEdit.id,
label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds),
svgIcon: userEdit.svgIcon,
svgIconInactive: userEdit.svgIconInactive,
isActive: userEdit.isActive,
} satisfies Complete<CoreUserEditingDefinitionAction>
})
case UserEditingType.FORM:
return {
return literal<CoreUserEditingDefinitionForm>({
type: UserEditingType.FORM,
id: userEdit.id,
label: wrapTranslatableMessageFromBlueprints(userEdit.label, blueprintIds),
schema: clone(userEdit.schema),
currentValues: clone(userEdit.currentValues),
translationNamespaces: unprotectStringArray(blueprintIds),
} satisfies Complete<CoreUserEditingDefinitionForm>
})
case UserEditingType.SOFIE:
return literal<UserEditingDefinitionSofieDefault>({
type: UserEditingType.SOFIE,
id: userEdit.id,
})
default:
assertNever(userEdit)
return undefined
Expand Down
3 changes: 3 additions & 0 deletions packages/webui/src/client/ui/RundownView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ import * as RundownResolver from '../lib/RundownResolver'
import { MAGIC_TIME_SCALE_FACTOR } from './SegmentTimeline/Constants'
import { SelectedElementProvider, SelectedElementsContext } from './RundownView/SelectedElementsContext'
import { PropertiesPanel } from './UserEditOperations/PropertiesPanel'
import { DragContextProvider } from './RundownView/DragContextProvider'

const REHEARSAL_MARGIN = 1 * 60 * 1000
const HIDE_NOTIFICATIONS_AFTER_MOUNT: number | undefined = 5000
Expand Down Expand Up @@ -3018,6 +3019,7 @@ const RundownViewContent = translateWithTracker<IPropsWithReady, IState, ITracke
return (
<RundownTimingProvider playlist={playlist} defaultDuration={Settings.defaultDisplayDuration}>
<StudioContext.Provider value={studio}>
<DragContextProvider t={t}>
<SelectedElementProvider>

Check failure on line 3023 in packages/webui/src/client/ui/RundownView.tsx

View workflow job for this annotation

GitHub Actions / Lint Package (webui)

Insert `↹`

Check failure on line 3023 in packages/webui/src/client/ui/RundownView.tsx

View workflow job for this annotation

GitHub Actions / Lint Package (webui)

Insert `↹`
<SelectedElementsContext.Consumer>

Check failure on line 3024 in packages/webui/src/client/ui/RundownView.tsx

View workflow job for this annotation

GitHub Actions / Lint Package (webui)

Insert `↹`

Check failure on line 3024 in packages/webui/src/client/ui/RundownView.tsx

View workflow job for this annotation

GitHub Actions / Lint Package (webui)

Insert `↹`
{(selectionContext) => {

Check failure on line 3025 in packages/webui/src/client/ui/RundownView.tsx

View workflow job for this annotation

GitHub Actions / Lint Package (webui)

Insert `↹`

Check failure on line 3025 in packages/webui/src/client/ui/RundownView.tsx

View workflow job for this annotation

GitHub Actions / Lint Package (webui)

Insert `↹`
Expand Down Expand Up @@ -3266,6 +3268,7 @@ const RundownViewContent = translateWithTracker<IPropsWithReady, IState, ITracke
}
</SelectedElementsContext.Consumer>
</SelectedElementProvider>
</DragContextProvider>
</StudioContext.Provider>
</RundownTimingProvider>
)
Expand Down
39 changes: 39 additions & 0 deletions packages/webui/src/client/ui/RundownView/DragContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { PartInstanceId, PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { createContext } from 'react'
import { PieceUi } from '../SegmentContainer/withResolvedSegment'

export interface IDragContext {
/**
* Indicate a drag operation on a piece has started
* @param piece The piece that is being dragged
* @param timeScale The current TimeScale of the segment
* @param position The position of the mouse
* @param elementOffset The x-coordinate of the element relative to the mouse position
* @param limitToSegment Whether the piece can be dragged to other segments (note: if the other segment does not have the right source layer the piece will look to have disappeared... consider omitting this is a todo)
*/
startDrag: (
piece: PieceUi,
timeScale: number,
position: { x: number; y: number },
elementOffset?: number,
limitToSegment?: SegmentId
) => void
/**
* Indicate the part the mouse is on has changed
* @param partId The part id that the mouse is currently hovering on
* @param segmentId The segment the part currenly hover is in
* @param position The position of the part in absolute coords to the screen
*/
setHoveredPart: (partId: PartInstanceId, segmentId: SegmentId, position: { x: number; y: number }) => void

/**
* PieceId of the piece that is being dragged
*/
pieceId: undefined | PieceInstanceId
/**
* The piece with any local overrides coming from dragging it around (i.e. changed renderedInPoint)
*/
piece: undefined | PieceUi
}

export const dragContext = createContext<IDragContext | undefined>(undefined) // slay.
145 changes: 145 additions & 0 deletions packages/webui/src/client/ui/RundownView/DragContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { PartInstanceId, PieceInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { PropsWithChildren, useRef, useState } from 'react'
import { dragContext, IDragContext } from './DragContext'
import { PieceUi } from '../SegmentContainer/withResolvedSegment'
import { doUserAction, UserAction } from '../../lib/clientUserAction'
import { MeteorCall } from '../../lib/meteorApi'
import { TFunction } from 'i18next'
import { UIParts } from '../Collections'
import { Segments } from '../../collections'
import { literal } from '../../lib/tempLib'
import { DefaultUserOperationRetimePiece, DefaultUserOperationsTypes } from '@sofie-automation/blueprints-integration'

const DRAG_TIMEOUT = 10000

interface Props {
t: TFunction
}

// notes: this doesn't limit dragging between rundowns right now but I'm not sure if the ingest stage will be happy with that - mint
export function DragContextProvider({ t, children }: PropsWithChildren<Props>): JSX.Element {
const [pieceId, setPieceId] = useState<undefined | PieceInstanceId>(undefined)
const [piece, setPiece] = useState<undefined | PieceUi>(undefined)

const partIdRef = useRef<undefined | PartInstanceId>(undefined)
const positionRef = useRef({ x: 0, y: 0 })
const segmentIdRef = useRef<undefined | SegmentId>(undefined)

const startDrag = (
ogPiece: PieceUi,
timeScale: number,
pos: { x: number; y: number },
elementOffset?: number,
limitToSegment?: SegmentId
) => {
if (pieceId) return // a drag is currently in progress....

const inPoint = ogPiece.renderedInPoint ?? 0
segmentIdRef.current = limitToSegment
positionRef.current = pos
setPieceId(ogPiece.instance._id)

let localPiece = ogPiece // keep a copy of the overriden piece because react does not let us access the state of the context easily

const onMove = (e: MouseEvent) => {
const newInPoint =
(!partIdRef.current ? inPoint : (elementOffset ?? 0) / timeScale) +
(e.clientX - positionRef.current.x) / timeScale

localPiece = {
...ogPiece,
instance: { ...ogPiece.instance, partInstanceId: partIdRef.current ?? ogPiece.instance.partInstanceId },
renderedInPoint: newInPoint,
}
setPiece(localPiece)
}

const cleanup = () => {
// unset state - note: for ux reasons this runs after the backend operation has returned a result
setPieceId(undefined)
setPiece(undefined)
partIdRef.current = undefined
segmentIdRef.current = undefined
}

const onMouseUp = (e: MouseEvent) => {
// detach from the mouse
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onMouseUp)

// process the drag
if (!localPiece || localPiece.renderedInPoint === ogPiece.renderedInPoint) return cleanup()

// find the parts so we can get their externalId
const startPartId = localPiece.instance.piece.startPartId // this could become a funny thing with infinites
const part = UIParts.findOne(startPartId)
const oldPart =
startPartId === ogPiece.instance.piece.startPartId ? part : UIParts.findOne(ogPiece.instance.piece.startPartId)
if (!part) return cleanup() // tough to continue without a parent for the piece

// find the Segment's External ID
const segment = Segments.findOne(part?.segmentId)
const oldSegment = part?.segmentId === oldPart?.segmentId ? segment : Segments.findOne(oldPart?.segmentId)
if (!segment) return

const operationTarget = {
segmentExternalId: oldSegment?.externalId,
partExternalId: oldPart?.externalId,
pieceExternalId: ogPiece.instance.piece.externalId,
}
doUserAction(
t,
e,
UserAction.EXECUTE_USER_OPERATION,
(e, ts) =>
MeteorCall.userAction.executeUserChangeOperation(
e,
ts,
part.rundownId,
operationTarget,
literal<DefaultUserOperationRetimePiece>({
id: DefaultUserOperationsTypes.RETIME_PIECE,
payload: {
segmentExternalId: segment.externalId,
partExternalId: part.externalId,

inPoint: localPiece.renderedInPoint ?? inPoint,
},
})
),
() => {
cleanup()
}
)
}

document.addEventListener('mousemove', onMove)
document.addEventListener('mouseup', onMouseUp)

setTimeout(() => {
// after the timeout we want to bail out in case something went wrong
document.removeEventListener('mousemove', onMove)
document.removeEventListener('mouseup', onMouseUp)

cleanup()
}, DRAG_TIMEOUT)
}
const setHoveredPart = (updatedPartId: PartInstanceId, segmentId: SegmentId, pos: { x: number; y: number }) => {
if (!pieceId) return
if (updatedPartId === piece?.instance.partInstanceId) return
if (segmentIdRef.current && segmentIdRef.current !== segmentId) return

partIdRef.current = updatedPartId
positionRef.current = pos
}

const ctx = literal<IDragContext>({
pieceId,
piece,

startDrag,
setHoveredPart,
})

return <dragContext.Provider value={ctx}>{children}</dragContext.Provider>
}
Loading

0 comments on commit 1284f9f

Please sign in to comment.