Skip to content

Commit

Permalink
feat: audio source (#761)
Browse files Browse the repository at this point in the history
* feat: add AudioSource support

* feat: handle counter

* wip

* fix: remap base path

* feat: added play_sound action

* feat: added play mode and drag n drop file on field

* feat: added drop styles

* feat: added tooltip

* chore: fix tests

* chore: playground assets

* chore: remove console.log

* chore: fix path in test file

* chore: log more details

* chore: show error message

* chore: feedback
  • Loading branch information
cazala authored Oct 4, 2023
1 parent 44bc935 commit beb3642
Show file tree
Hide file tree
Showing 34 changed files with 532 additions and 154 deletions.
14 changes: 7 additions & 7 deletions packages/@dcl/inspector/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/@dcl/inspector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@babylonjs/inspector": "^6.18.0",
"@babylonjs/loaders": "^6.18.0",
"@babylonjs/materials": "^6.18.0",
"@dcl/asset-packs": "^0.0.0-20231003180441.commit-56a3910",
"@dcl/asset-packs": "^0.0.0-20231003201348.commit-00db039",
"@dcl/ecs": "file:../ecs",
"@dcl/ecs-math": "2.0.2",
"@dcl/mini-rpc": "^1.0.7",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import MoreOptionsMenu from '../MoreOptionsMenu'
import { AddButton } from '../AddButton'
import { Button } from '../../Button'

import { PlaySoundAction } from './PlaySoundAction'
import { TweenAction } from './TweenAction'
import { isValidTween } from './TweenAction/utils'
import { Props } from './types'
Expand Down Expand Up @@ -235,7 +236,7 @@ export default withSdk<Props>(
)

const handleChangeTween = useCallback(
(tween: ActionPayload['start_tween'], idx: number) => {
(tween: ActionPayload<ActionType.START_TWEEN>, idx: number) => {
setActions((prev: Action[]) => {
const data = [...prev]
data[idx] = {
Expand All @@ -248,6 +249,20 @@ export default withSdk<Props>(
[setActions]
)

const handleChangeSound = useCallback(
(value: ActionPayload<ActionType.PLAY_SOUND>, idx: number) => {
setActions((prev: Action[]) => {
const data = [...prev]
data[idx] = {
...data[idx],
jsonPayload: getJson<ActionType.PLAY_SOUND>(value)
}
return data
})
},
[setActions]
)

const handleChangeCounter = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLInputElement>, idx: number) => {
setActions((prev: Action[]) => {
Expand Down Expand Up @@ -394,7 +409,7 @@ export default withSdk<Props>(
return (
<TweenAction
tween={getPartialPayload<ActionType.START_TWEEN>(action)}
onUpdateTween={(tween: ActionPayload['start_tween']) => handleChangeTween(tween, idx)}
onUpdateTween={(tween: ActionPayload<ActionType.START_TWEEN>) => handleChangeTween(tween, idx)}
/>
)
}
Expand All @@ -412,6 +427,14 @@ export default withSdk<Props>(
</div>
) : null
}
case ActionType.PLAY_SOUND: {
return (
<PlaySoundAction
value={getPartialPayload<ActionType.PLAY_SOUND>(action)}
onUpdate={(value: ActionPayload<ActionType.PLAY_SOUND>) => handleChangeSound(value, idx)}
/>
)
}
default: {
// TODO: handle generic schemas with something like <JsonSchemaField/>
return null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.PlaySoundActionContainer {
width: 100%;
}

.PlaySoundActionContainer .SelectField {
width: 100%;
}

.PlaySoundActionContainer .SelectField {
width: 100%;
}

.PlaySoundActionContainer .field.drop {
border: 1px solid white;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useDrop } from 'react-dnd'
import { VscQuestion as QuestionIcon } from 'react-icons/vsc'
import { Popup } from 'decentraland-ui/dist/components/Popup/Popup'
import { ActionPayload, ActionType } from '@dcl/asset-packs'
import { recursiveCheck } from 'jest-matcher-deep-close-to/lib/recursiveCheck'

import { DropTypesEnum, ProjectAssetDrop, getNode } from '../../../../lib/sdk/drag-drop'
import { withAssetDir } from '../../../../lib/data-layer/host/fs-utils'
import { isAudio } from '../../AudioSourceInspector/utils'
import { TextField } from '../../TextField'
import { SelectField } from '../../SelectField'
import { useAppSelector } from '../../../../redux/hooks'
import { selectAssetCatalog } from '../../../../redux/app'
import { isValid } from './utils'
import type { Props } from './types'

import './PlaySoundAction.css'

enum PLAY_MODE {
PLAY_ONCE = 'play-once',
LOOP = 'loop'
}

const options = [
{
label: 'Play Once',
value: PLAY_MODE.PLAY_ONCE
},
{
label: 'Loop',
value: PLAY_MODE.LOOP
}
]

const PlaySoundAction: React.FC<Props> = ({ value, onUpdate }: Props) => {
const [payload, setPayload] = useState<ActionPayload<ActionType.PLAY_SOUND>>({
...value
})

const files = useAppSelector(selectAssetCatalog)

const removeBase = useCallback(
(path: string) => {
return files?.basePath ? path.replace(files.basePath + '/', '') : path
},
[files]
)

const addBase = useCallback(
(path: string) => {
return files?.basePath ? `${files.basePath}/${path}` : path
},
[files]
)

useEffect(() => {
if (!recursiveCheck(payload, value, 2) || !isValid(payload)) return
onUpdate(payload)
}, [payload, onUpdate])

const handleChangeSrc = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
setPayload({ ...payload, src: addBase(value) })
},
[payload, setPayload]
)

const handleChangePlayMode = useCallback(
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
setPayload({ ...payload, loop: value === PLAY_MODE.LOOP })
},
[payload, setPayload]
)

const [{ isHover }, drop] = useDrop(
() => ({
accept: [DropTypesEnum.ProjectAsset],
drop: ({ value, context }: ProjectAssetDrop, monitor) => {
if (monitor.didDrop()) return
const node = context.tree.get(value)!
const audio = getNode(node, context.tree, isAudio)
if (audio) {
setPayload({ ...payload, src: withAssetDir(audio.asset.src) })
}
},
canDrop: ({ value, context }: ProjectAssetDrop) => {
const node = context.tree.get(value)!
return !!getNode(node, context.tree, isAudio)
},
collect: (monitor) => ({
isHover: monitor.canDrop() && monitor.isOver()
})
}),
[files]
)

const error = useMemo(() => {
return !files || !files.assets.some(($) => $.path === payload.src)
}, [files, payload])

const renderPathInfo = () => {
return (
<Popup
content={<>You can drag and drop an audio file from the Local Assets</>}
trigger={<QuestionIcon size={16} />}
position="right center"
on="hover"
hideOnScroll
hoverable
/>
)
}

return (
<div className="PlaySoundActionContainer">
<div className="row">
<div className="field" ref={drop}>
<label>Path {renderPathInfo()}</label>
<TextField value={removeBase(payload.src)} onChange={handleChangeSrc} error={error} drop={isHover} />
</div>
<div className="field">
<label>Play Mode</label>
<SelectField
value={payload.loop ? PLAY_MODE.LOOP : PLAY_MODE.PLAY_ONCE}
options={options}
onChange={handleChangePlayMode}
/>
</div>
</div>
</div>
)
}

export default React.memo(PlaySoundAction)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import PlaySoundAction from './PlaySoundAction'
export { PlaySoundAction }
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ActionPayload, ActionType } from '@dcl/asset-packs'

export interface Props {
value: ActionPayload[ActionType.PLAY_SOUND]
onUpdate: (value: ActionPayload[ActionType.PLAY_SOUND]) => void
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ActionPayload, ActionType } from '@dcl/asset-packs'

export function isValid(payload: ActionPayload<ActionType.PLAY_SOUND>) {
return !!payload.src
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const TweenAction: React.FC<Props> = ({ tween: tweenProp, onUpdateTween }: Props
z: 0
},
relative: true,
interpolationType: '',
interpolationType: InterpolationType.LINEAR,
duration: 1,
...tweenProp
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useCallback } from 'react'
import { Item } from 'react-contexify'
import { useDrop } from 'react-dnd'
import { AiFillDelete as DeleteIcon } from 'react-icons/ai'
import cx from 'classnames'

import { ContextMenu as Menu } from '../../ContexMenu'
import { withContextMenu } from '../../../hoc/withContextMenu'
import { WithSdkProps, withSdk } from '../../../hoc/withSdk'
import { useHasComponent } from '../../../hooks/sdk/useHasComponent'
import { useComponentInput } from '../../../hooks/sdk/useComponentInput'
import { useContextMenu } from '../../../hooks/sdk/useContextMenu'
import { ProjectAssetDrop, getNode } from '../../../lib/sdk/drag-drop'
import { withAssetDir } from '../../../lib/data-layer/host/fs-utils'
import { useAppSelector } from '../../../redux/hooks'
import { selectAssetCatalog } from '../../../redux/app'
import { Block } from '../../Block'
import { Container } from '../../Container'
import { TextField } from '../TextField'
import { Props } from './types'
import { fromAudioSource, toAudioSource, isValidInput, isAudio } from './utils'

const DROP_TYPES = ['project-asset']

export default withSdk<Props>(
withContextMenu<WithSdkProps & Props>(({ sdk, entity, contextMenuId }) => {
const files = useAppSelector(selectAssetCatalog)
const { handleAction } = useContextMenu()
const { AudioSource } = sdk.components

const hasAudioSource = useHasComponent(entity, AudioSource)
const handleInputValidation = useCallback(
({ audioClipUrl }: { audioClipUrl: string }) => !!files && isValidInput(files, audioClipUrl),
[files]
)
const { getInputProps, isValid } = useComponentInput(
entity,
AudioSource,
fromAudioSource(files?.basePath ?? ''),
toAudioSource(files?.basePath ?? ''),
handleInputValidation,
[files]
)

const handleRemove = useCallback(async () => {
sdk.operations.removeComponent(entity, AudioSource)
await sdk.operations.dispatch()
}, [])
const handleDrop = useCallback(async (audioClipUrl: string) => {
const { operations } = sdk
operations.updateValue(AudioSource, entity, { audioClipUrl })
await operations.dispatch()
}, [])

const [{ isHover }, drop] = useDrop(
() => ({
accept: DROP_TYPES,
drop: ({ value, context }: ProjectAssetDrop, monitor) => {
if (monitor.didDrop()) return
const node = context.tree.get(value)!
const model = getNode(node, context.tree, isAudio)
if (model) void handleDrop(withAssetDir(model.asset.src))
},
canDrop: ({ value, context }: ProjectAssetDrop) => {
const node = context.tree.get(value)!
return !!getNode(node, context.tree, isAudio)
},
collect: (monitor) => ({
isHover: monitor.canDrop() && monitor.isOver()
})
}),
[files]
)

if (!hasAudioSource) return null

const playing = getInputProps('playing', (e) => e.target.checked)
const loop = getInputProps('loop', (e) => e.target.checked)

return (
<Container label="AudioSource" className={cx('AudioSource', { hover: isHover })}>
<Menu id={contextMenuId}>
<Item id="delete" onClick={handleAction(handleRemove)}>
<DeleteIcon /> Delete
</Item>
</Menu>
<Block label="Path" ref={drop}>
<TextField type="text" {...getInputProps('audioClipUrl')} error={files && !isValid} drop={isHover} />
</Block>
<Block label="Playback">
<TextField label="Start playing" type="checkbox" checked={!!playing.value} {...playing} />
<TextField label="Loop" type="checkbox" checked={!!loop.value} {...loop} />
</Block>
</Container>
)
})
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import AudioSourceInspector from './AudioSourceInspector'
export { AudioSourceInspector }
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Entity } from '@dcl/ecs'

export interface Props {
entity: Entity
}

export type AudioSourceInput = {
audioClipUrl: string
playing?: boolean
loop?: boolean
}
Loading

0 comments on commit beb3642

Please sign in to comment.