/
githubmirror
/
gutenberg
Обзор
Документация
Войти
/
githubmirror
/
gutenberg
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
trunk
packages/block-library/src/playlist/edit.js
724 строки
19 KB
Dave Smith
Playlist: Improve audio conversion and track selection (#80926)
10 авг 2026, 14:46
Не верифицирован
10 авг 2026, 14:46
ffd97c0
Код
Авторство
О чём код?
import clsx from 'clsx'; import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; import { store as blockEditorStore, MediaPlaceholder, MediaReplaceFlow, BlockIcon, useBlockProps, useInnerBlocksProps, BlockControls, InspectorControls, __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, } from '@wordpress/block-editor'; import { ToggleControl, Disabled, SelectControl, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as noticesStore } from '@wordpress/notices'; import { __, _x } from '@wordpress/i18n'; import { playlist as icon } from '@wordpress/icons'; import { createBlock } from '@wordpress/blocks'; import { createBlobURL } from '@wordpress/blob'; import { Caption } from '../utils/caption'; import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; import { WaveformPlayer } from '../utils/waveform-player'; import { PlaylistContext } from './context'; import { getTrackAttributes } from './utils'; const ALLOWED_MEDIA_TYPES = [ 'audio' ]; const AUDIO_FILE_EXTENSION = /\.(aac|aif|aiff|flac|m4a|m4b|mp3|oga|ogg|opus|wav|weba)$/i; const DEFAULT_WAVEFORM_STYLE = 'bars'; const FILE_LIST_OBJECT_NAME = '[object FileList]'; const WAVEFORM_STYLE_OPTIONS = [ { label: _x( 'Bars', 'waveform style option' ), value: 'bars' }, { label: _x( 'Mirror', 'waveform style option' ), value: 'mirror' }, { label: _x( 'Line', 'waveform style option' ), value: 'line' }, { label: _x( 'Blocks', 'waveform style option' ), value: 'blocks' }, { label: _x( 'Dots', 'waveform style option' ), value: 'dots' }, { label: _x( 'Seekbar', 'waveform style option' ), value: 'seekbar' }, ]; function isFile( value ) { return ( Object.prototype.toString.call( value ) === '[object File]' || ( typeof File !== 'undefined' && value instanceof File ) ); } function isAudioFile( file ) { return file.type ? file.type.startsWith( 'audio/' ) : AUDIO_FILE_EXTENSION.test( file.name ); } function getTrackIdentifier( track ) { return track.id ?? track.src ?? track.blob; } const PlaylistEdit = ( { attributes, setAttributes, isSelected, insertBlocksAfter, clientId, } ) => { const { order, showTracklist, showNumbers, showImages, showPlayButtonArtwork, showArtists, showTrackLength, waveformStyle = DEFAULT_WAVEFORM_STYLE, waveformColor, waveformGradient, waveformBackgroundColor, waveformBackgroundGradient, } = attributes; const blockProps = useBlockProps(); const waveformPanelId = `${ clientId }-waveform`; const { replaceInnerBlocks, selectBlock } = useDispatch( blockEditorStore ); const { createErrorNotice } = useDispatch( noticesStore ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); const colorGradientSettings = useMultipleOriginColorsAndGradients(); const colors = useMemo( () => colorGradientSettings.colors.flatMap( ( origin ) => origin?.colors ?? [] ), [ colorGradientSettings.colors ] ); const gradients = useMemo( () => colorGradientSettings.gradients.flatMap( ( origin ) => origin?.gradients ?? [] ), [ colorGradientSettings.gradients ] ); const hasColors = colors.length > 0 || ! colorGradientSettings.disableCustomColors; const hasGradients = gradients.length > 0 || ! colorGradientSettings.disableCustomGradients; const waveformGradientValue = waveformGradient; const waveformBackgroundGradientValue = waveformBackgroundGradient; let waveformColorGradientChange; let waveformBackgroundColorGradientChange; const onUploadError = useCallback( ( message ) => { createErrorNotice( message, { type: 'snackbar' } ); }, [ createErrorNotice ] ); const [ currentTrackClientId, setCurrentTrackClientId ] = useState( null ); const { innerBlockTracks } = useSelect( ( select ) => { const { getBlock: _getBlock } = select( blockEditorStore ); return { innerBlockTracks: _getBlock( clientId )?.innerBlocks ?? [], }; }, [ clientId ] ); // Create a list of tracks from the inner blocks, // but skip blocks that do not have a source, such as the media placeholder. const validTracks = useMemo( () => innerBlockTracks.filter( ( block ) => !! block.attributes.src || !! block.attributes.blob ), [ innerBlockTracks ] ); const tracks = useMemo( () => validTracks.map( ( block ) => ( { ...block.attributes, clientId: block.clientId, } ) ), [ validTracks ] ); useEffect( () => { if ( validTracks.length === 0 ) { if ( currentTrackClientId !== null ) { setCurrentTrackClientId( null ); } return; } const currentTrackExists = validTracks.some( ( block ) => block.clientId === currentTrackClientId ); if ( ! currentTrackExists ) { setCurrentTrackClientId( validTracks[ 0 ].clientId ); } }, [ currentTrackClientId, setCurrentTrackClientId, validTracks ] ); const createTrackBlocks = useCallback( ( media ) => { if ( ! media ) { return []; } let mediaItems = [ media ]; if ( Object.prototype.toString.call( media ) === FILE_LIST_OBJECT_NAME ) { mediaItems = Array.from( media ); } else if ( Array.isArray( media ) ) { mediaItems = media; } let hasInvalidFile = false; const blocks = mediaItems .map( ( mediaItem ) => { if ( isFile( mediaItem ) ) { if ( ! isAudioFile( mediaItem ) ) { hasInvalidFile = true; return null; } return createBlock( 'core/playlist-track', { blob: createBlobURL( mediaItem ), title: mediaItem.name, } ); } const track = getTrackAttributes( mediaItem ); return track.src ? createBlock( 'core/playlist-track', track ) : null; } ) .filter( Boolean ); if ( hasInvalidFile ) { onUploadError( __( 'Only audio files can be added to a playlist.' ) ); } return blocks; }, [ onUploadError ] ); const onSelectTracks = useCallback( ( media ) => { const newBlocks = createTrackBlocks( media ); if ( newBlocks.length === 0 ) { return; } setCurrentTrackClientId( newBlocks[ 0 ]?.clientId ?? null ); // Replace the inner blocks with the new tracks. replaceInnerBlocks( clientId, newBlocks ); }, [ clientId, createTrackBlocks, replaceInnerBlocks, setCurrentTrackClientId, ] ); const onAddTracks = useCallback( ( media ) => { const existingIds = new Set( validTracks .map( ( block ) => getTrackIdentifier( block.attributes ) ) .filter( Boolean ) ); const newBlocks = createTrackBlocks( media ).filter( ( block ) => ! existingIds.has( getTrackIdentifier( block.attributes ) ) ); if ( newBlocks.length === 0 ) { return; } const nextBlocks = [ ...validTracks, ...newBlocks ]; setCurrentTrackClientId( newBlocks[ 0 ].clientId ); replaceInnerBlocks( clientId, nextBlocks ); selectBlock( newBlocks[ 0 ].clientId ); }, [ clientId, createTrackBlocks, replaceInnerBlocks, selectBlock, setCurrentTrackClientId, validTracks, ] ); const playlistContext = useMemo( () => ( { currentTrackClientId, setCurrentTrackClientId, } ), [ currentTrackClientId, setCurrentTrackClientId ] ); // Get current track data by finding the track with matching client ID. const currentTrackData = tracks.find( ( track ) => track.clientId === currentTrackClientId ) ?? tracks[ 0 ]; // Handle track end - advance to next track or loop to first. const onTrackEnded = useCallback( () => { const currentIndex = tracks.findIndex( ( track ) => track.clientId === currentTrackClientId ); const nextTrack = tracks[ currentIndex + 1 ] || tracks[ 0 ]; if ( nextTrack?.clientId ) { setCurrentTrackClientId( nextTrack.clientId ); } }, [ currentTrackClientId, setCurrentTrackClientId, tracks ] ); const onChangeOrder = useCallback( ( trackOrder ) => { const sortedBlocks = [ ...innerBlockTracks ].sort( ( a, b ) => { const titleA = a.attributes.title || ''; const titleB = b.attributes.title || ''; if ( trackOrder === 'asc' ) { return titleA.localeCompare( titleB ); } return titleB.localeCompare( titleA ); } ); replaceInnerBlocks( clientId, sortedBlocks ); setCurrentTrackClientId( sortedBlocks[ 0 ]?.clientId ?? null ); setAttributes( { order: trackOrder, } ); }, [ clientId, innerBlockTracks, replaceInnerBlocks, setAttributes, setCurrentTrackClientId, ] ); function toggleAttribute( attribute ) { return ( newValue ) => { setAttributes( { [ attribute ]: newValue } ); }; } const onChangeWaveformStyle = useCallback( ( newWaveformStyle ) => { setAttributes( { waveformStyle: newWaveformStyle === DEFAULT_WAVEFORM_STYLE ? undefined : newWaveformStyle, } ); }, [ setAttributes ] ); function updateWaveformColor( colorValue ) { const isSettingColor = colorValue !== undefined; if ( ! isSettingColor && waveformColorGradientChange === 'gradient' ) { waveformColorGradientChange = undefined; return; } waveformColorGradientChange = 'color'; setAttributes( { waveformColor: colorValue, waveformGradient: undefined, } ); } function updateWaveformGradient( gradientValue ) { const isSettingGradient = gradientValue !== undefined; if ( ! isSettingGradient && waveformColorGradientChange === 'color' ) { waveformColorGradientChange = undefined; return; } waveformColorGradientChange = 'gradient'; setAttributes( { waveformGradient: gradientValue, waveformColor: undefined, } ); } function updateWaveformBackgroundColor( colorValue ) { const isSettingColor = colorValue !== undefined; if ( ! isSettingColor && waveformBackgroundColorGradientChange === 'gradient' ) { waveformBackgroundColorGradientChange = undefined; return; } waveformBackgroundColorGradientChange = 'color'; setAttributes( { waveformBackgroundColor: colorValue, waveformBackgroundGradient: undefined, } ); } function updateWaveformBackgroundGradient( gradientValue ) { const isSettingGradient = gradientValue !== undefined; if ( ! isSettingGradient && waveformBackgroundColorGradientChange === 'color' ) { waveformBackgroundColorGradientChange = undefined; return; } waveformBackgroundColorGradientChange = 'gradient'; setAttributes( { waveformBackgroundGradient: gradientValue, waveformBackgroundColor: undefined, } ); } const colorSettings = []; if ( hasColors || hasGradients ) { colorSettings.push( { colorValue: hasColors ? waveformColor : undefined, gradientValue: hasGradients ? waveformGradientValue : undefined, label: __( 'Waveform & Play button' ), onColorChange: hasColors ? updateWaveformColor : undefined, onGradientChange: hasGradients ? updateWaveformGradient : undefined, isShownByDefault: true, clearable: true, enableAlpha: true, resetAllFilter: () => ( { waveformColor: undefined, waveformGradient: undefined, } ), }, { colorValue: hasColors ? waveformBackgroundColor : undefined, gradientValue: hasGradients ? waveformBackgroundGradientValue : undefined, label: __( 'Waveform background' ), onColorChange: hasColors ? updateWaveformBackgroundColor : undefined, onGradientChange: hasGradients ? updateWaveformBackgroundGradient : undefined, isShownByDefault: true, clearable: true, enableAlpha: true, resetAllFilter: () => ( { waveformBackgroundColor: undefined, waveformBackgroundGradient: undefined, } ), } ); } const innerBlocksProps = useInnerBlocksProps( blockProps, { __experimentalAppenderTagName: 'li', renderAppender: false, } ); if ( tracks.length === 0 ) { return ( <div { ...blockProps } className={ clsx( 'is-placeholder', blockProps.className ) } > <MediaPlaceholder icon={ <BlockIcon icon={ icon } /> } labels={ { title: __( 'Playlist' ), instructions: __( 'Upload an audio file or pick one from your media library.' ), } } onSelect={ onSelectTracks } accept="audio/*" multiple="add" handleUpload={ false } allowedTypes={ ALLOWED_MEDIA_TYPES } onError={ onUploadError } /> </div> ); } return ( <> <BlockControls group="other" __experimentalShareWithChildBlocks> <MediaReplaceFlow name={ __( 'Add track' ) } onSelect={ onAddTracks } accept="audio/*" multiple="add" handleUpload={ false } allowedTypes={ ALLOWED_MEDIA_TYPES } onError={ onUploadError } /> </BlockControls> <InspectorControls> <ToolsPanel label={ __( 'Settings' ) } resetAll={ () => { setAttributes( { showTracklist: true, showArtists: true, showNumbers: true, showTrackLength: true, showImages: true, showPlayButtonArtwork: false, order: 'asc', } ); } } dropdownMenuProps={ dropdownMenuProps } > <ToolsPanelItem label={ __( 'Show tracklist' ) } isShownByDefault hasValue={ () => showTracklist !== true } onDeselect={ () => setAttributes( { showTracklist: true } ) } > <ToggleControl label={ __( 'Show tracklist' ) } onChange={ toggleAttribute( 'showTracklist' ) } checked={ showTracklist } /> </ToolsPanelItem> { showTracklist && ( <> <ToolsPanelItem label={ __( 'Show artist name in tracklist' ) } isShownByDefault hasValue={ () => showArtists !== true } onDeselect={ () => setAttributes( { showArtists: true } ) } > <ToggleControl label={ __( 'Show artist name in tracklist' ) } onChange={ toggleAttribute( 'showArtists' ) } checked={ showArtists } /> </ToolsPanelItem> <ToolsPanelItem label={ __( 'Show track numbers in tracklist' ) } isShownByDefault hasValue={ () => showNumbers !== true } onDeselect={ () => setAttributes( { showNumbers: true } ) } > <ToggleControl label={ __( 'Show track numbers in tracklist' ) } onChange={ toggleAttribute( 'showNumbers' ) } checked={ showNumbers } /> </ToolsPanelItem> <ToolsPanelItem label={ __( 'Show track duration in tracklist' ) } isShownByDefault hasValue={ () => showTrackLength !== true } onDeselect={ () => setAttributes( { showTrackLength: true } ) } > <ToggleControl label={ __( 'Show track duration in tracklist' ) } onChange={ toggleAttribute( 'showTrackLength' ) } checked={ showTrackLength } /> </ToolsPanelItem> </> ) } <ToolsPanelItem label={ __( 'Show tracklist images' ) } isShownByDefault hasValue={ () => showImages !== true } onDeselect={ () => setAttributes( { showImages: true } ) } > <ToggleControl label={ __( 'Show tracklist images' ) } onChange={ toggleAttribute( 'showImages' ) } checked={ showImages } /> </ToolsPanelItem> <ToolsPanelItem label={ __( 'Show track image on play button' ) } isShownByDefault hasValue={ () => showPlayButtonArtwork === true } onDeselect={ () => setAttributes( { showPlayButtonArtwork: false } ) } > <ToggleControl label={ __( 'Show track image on play button' ) } onChange={ toggleAttribute( 'showPlayButtonArtwork' ) } checked={ showPlayButtonArtwork === true } /> </ToolsPanelItem> <ToolsPanelItem label={ __( 'Order' ) } isShownByDefault hasValue={ () => order !== 'asc' } onDeselect={ () => setAttributes( { order: 'asc' } ) } > <SelectControl label={ __( 'Order' ) } value={ order } options={ [ { label: __( 'Descending' ), value: 'desc' }, { label: __( 'Ascending' ), value: 'asc' }, ] } onChange={ ( value ) => onChangeOrder( value ) } /> </ToolsPanelItem> </ToolsPanel> </InspectorControls> <InspectorControls group="styles"> <ToolsPanel label={ __( 'Waveform' ) } resetAll={ () => { setAttributes( { waveformStyle: undefined, waveformColor: undefined, waveformGradient: undefined, waveformBackgroundColor: undefined, waveformBackgroundGradient: undefined, } ); } } panelId={ waveformPanelId } dropdownMenuProps={ dropdownMenuProps } > { colorSettings.length > 0 && ( <div className="wp-block-playlist__waveform-color-controls"> <ColorGradientSettingsDropdown __experimentalIsRenderedInSidebar settings={ colorSettings } panelId={ waveformPanelId } { ...colorGradientSettings } /> </div> ) } <ToolsPanelItem label={ __( 'Shape' ) } isShownByDefault hasValue={ () => waveformStyle !== DEFAULT_WAVEFORM_STYLE } onDeselect={ () => onChangeWaveformStyle( DEFAULT_WAVEFORM_STYLE ) } panelId={ waveformPanelId } > <SelectControl label={ __( 'Shape' ) } value={ waveformStyle } options={ WAVEFORM_STYLE_OPTIONS } onChange={ onChangeWaveformStyle } /> </ToolsPanelItem> </ToolsPanel> </InspectorControls> <figure { ...blockProps }> <MediaPlaceholder onSelect={ onAddTracks } accept="audio/*" multiple="add" handleUpload={ false } disableMediaButtons allowedTypes={ ALLOWED_MEDIA_TYPES } onError={ onUploadError } /> <Disabled isDisabled={ ! isSelected }> <WaveformPlayer src={ currentTrackData?.src } title={ currentTrackData?.title } artist={ currentTrackData?.artist } image={ currentTrackData?.image } imageAlt={ currentTrackData?.imageAlt } waveformStyle={ waveformStyle } color={ waveformColor } gradient={ waveformGradientValue } backgroundColor={ waveformBackgroundColor } backgroundGradient={ waveformBackgroundGradientValue } onEnded={ onTrackEnded } showPlayButtonArtwork={ showPlayButtonArtwork === true } /> </Disabled> <ol className={ clsx( 'wp-block-playlist__tracklist', { 'wp-block-playlist__tracklist-is-hidden': ! showTracklist, 'wp-block-playlist__tracklist-show-numbers': showNumbers, 'wp-block-playlist__tracklist-length-is-hidden': ! showTrackLength, } ) } > <PlaylistContext.Provider value={ playlistContext }> { innerBlocksProps.children } </PlaylistContext.Provider> </ol> <Caption attributes={ attributes } setAttributes={ setAttributes } isSelected={ isSelected } insertBlocksAfter={ insertBlocksAfter } label={ __( 'Playlist caption text' ) } showToolbarButton={ isSelected } style={ { marginTop: 16 } } /> </figure> </> ); }; export default PlaylistEdit;