Skip to content

Commit

Permalink
Add paste functionality to the rundown plugin
Browse files Browse the repository at this point in the history
Signed-off-by: Axel Boberg <[email protected]>
  • Loading branch information
axelboberg committed Jan 19, 2024
1 parent 112d22c commit f8b957d
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 3 deletions.
11 changes: 10 additions & 1 deletion app/utils/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
* @param { String } str A string to copy
* @returns { Promise.<Boolean> }
*/
export async function copyText (str) {
export function copyText (str) {
return navigator.clipboard.writeText(str)
}

/**
* Read the string stored in the clipboard,
* will return an empty string if the clipboard is empty
* @returns { Promise.<String> }
*/
export function readText () {
return navigator.clipboard.readText()
}
6 changes: 5 additions & 1 deletion app/utils/shortcuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const timeoutOnIdle = (function () {
*
* - Lower case keys are all transformed to upper case
*
* @example
* 'a' -> 'A'
* 'Meta' -> 'Meta'
*
* @param { String } key The key to normalize
* @returns { String }
*/
Expand Down Expand Up @@ -107,5 +111,5 @@ export async function registerKeyDown (e) {
* @param { KeyboardEvent } e
*/
export function registerKeyUp (e) {
keys.delete(normalize(e.key))
keys.clear()
}
6 changes: 6 additions & 0 deletions plugins/rundown/app/components/RundownListItem/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export function RundownListItem ({
setIndicateIsPlaying(true)
}, [item?.state, item?.didStartPlayingAt])

async function handlePaste () {
const items = await clipboard.readJson()
bridge.commands.executeCommand('rundown.pasteItems', items, rundownId, index + 1)
}

return (
<div
className={`RundownListItem ${isDraggedOver ? 'is-draggedOver' : ''} ${isSelected ? 'is-selected' : ''}`}
Expand Down Expand Up @@ -142,6 +147,7 @@ export function RundownListItem ({
selection.length <= 1 &&
<ContextMenuItem text='Copy id' onClick={() => handleCopyId()} />
}
<ContextMenuItem text='Paste' onClick={() => handlePaste()} />
<ContextMenuDivider />
<ContextMenuItem text='Add after'>
<ContextAddMenu onAdd={newItemId => handleAdd(newItemId)} />
Expand Down
25 changes: 24 additions & 1 deletion plugins/rundown/app/utils/clipboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@
* @param { String } str A string to copy
* @returns { Promise.<Boolean> }
*/
export async function copyText (str) {
export function copyText (str) {
return navigator.clipboard.writeText(str)
}

/**
* Read the string stored in the clipboard,
* will return an empty string if the clipboard is empty
* @returns { Promise.<String> }
*/
export function readText () {
return navigator.clipboard.readText()
}

/**
* Read the contents of the clipboard as a json object,
* will return undefined if unable to parse the data
* @returns { Promise.<Object | undefined> }
*/
export async function readJson () {
try {
const str = await readText()
return JSON.parse(str)
} catch (_) {
return undefined
}
}
39 changes: 39 additions & 0 deletions plugins/rundown/app/views/Rundown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { ContextAddMenu } from '../components/ContextAddMenu'

import { ContextMenu } from '../../../../app/components/ContextMenu'
import { ContextMenuItem } from '../../../../app/components/ContextMenuItem'
import { ContextMenuDivider } from '../../../../app/components/ContextMenuDivider'

import * as config from '../config'
import * as clipboard from '../utils/clipboard'

export function Rundown () {
const [contextPos, setContextPos] = React.useState()
Expand All @@ -23,12 +25,49 @@ export function Rundown () {
bridge.commands.executeCommand('rundown.appendItem', rundownId, itemId)
}

async function handlePaste () {
const items = await clipboard.readJson()
const selection = await bridge.client.getSelection()

/*
Paste the items into the
selected item's parent
*/
if (selection.length) {
const item = await bridge.items.getItem(selection[0])
bridge.commands.executeCommand('rundown.pasteItems', items, item.parent)

/*
Paste the items into the current
rundown if no item is selected
*/
} else {
bridge.commands.executeCommand('rundown.pasteItems', items, rundownId)
}
}

React.useEffect(() => {
function onShortcut (e) {
switch (e.detail.id) {
case 'paste':
handlePaste()
break
}
}
window.addEventListener('shortcut', onShortcut)
return () => {
window.removeEventListener('shortcut', onShortcut)
}
}, [rundownId])

return (
<div className='View' onContextMenu={e => handleContextMenu(e)}>
{
contextPos
? (
<ContextMenu x={contextPos[0]} y={contextPos[1]} onClose={() => setContextPos(undefined)}>
<ContextMenuItem text='Paste' onClick={() => handlePaste()} />
<ContextMenuDivider />
<ContextMenuItem text='Add'>
<ContextAddMenu onAdd={newItemId => handleItemCreate(newItemId)}/>
</ContextMenuItem>
Expand Down
64 changes: 64 additions & 0 deletions plugins/rundown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,70 @@ exports.activate = async () => {
}
bridge.commands.registerCommand('rundown.copyItems', copyItems)

/**
* Paste items into the
* specified parent at index
*
* Will create new item id's to avoid collisions with
* already existing items but keep the item structure
*
* @param { Any[] } items An array of item objects from the clipboard
* @param { String } parentId The id of the parent to paste into
* @param { Number | undefined } index The index at which to paste the first item
* @returns { Promise.<void> }
*/
async function pasteItems (items, parentId, index) {
if (!items || !Array.isArray(items)) {
return
}

const idMappings = {}

const promises = items.map(async item => {
const newId = await bridge.items.createItem(item.type)
idMappings[item.id] = newId
})

await Promise.all(promises)

let i = -1
for (const item of items) {
i++
if (item.children && Array.isArray(item.children)) {
item.children = item.children.map(child => idMappings[child])
}

item.id = idMappings[item.id]
await bridge.items.applyItem(item.id, {
...item,
parent: undefined
})

/*
If this is a nested item, append it into its
new parent that was just created
*/
if (item.parent && idMappings[item.parent]) {
await appendItem(idMappings[item.parent], item.id)
return
}

/*
If this is a top level item,
paste it into the specified parent and
if specified move it to the correct index
*/
if (item.parent && !idMappings[item.parent]) {
await appendItem(parentId, item.id)

if (index != null) {
moveItem(parentId, index + i, item.id)
}
}
}
}
bridge.commands.registerCommand('rundown.pasteItems', pasteItems)

/**
* Append an item to
* a new parent, will remove any current relations
Expand Down

0 comments on commit f8b957d

Please sign in to comment.