diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 7fd6966211c20b..b278e29d4555a9 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -23,6 +23,7 @@ export { default as URLInput } from './url-input'; export { default as BlockInvalidWarning } from './block-list/block-invalid-warning'; export { default as Caption } from './caption'; export { BottomSheetSettings, BlockSettingsButton } from './block-settings'; +export { default as VideoPlayer, VIDEO_ASPECT_RATIO } from './video-player'; // Content Related Components export { default as BlockList } from './block-list'; diff --git a/packages/block-editor/src/components/media-placeholder/index.native.js b/packages/block-editor/src/components/media-placeholder/index.native.js index c4579e78fe6bfa..3680401362a4cb 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -52,6 +52,8 @@ function MediaPlaceholder( props ) { instructions = __( 'ADD IMAGE' ); } else if ( isVideo ) { instructions = __( 'ADD VIDEO' ); + } else { + instructions = __( 'ADD IMAGE OR VIDEO' ); } } diff --git a/packages/block-editor/src/components/media-placeholder/styles.native.scss b/packages/block-editor/src/components/media-placeholder/styles.native.scss index 03c7c73b2edfc9..a0b7445debf66b 100644 --- a/packages/block-editor/src/components/media-placeholder/styles.native.scss +++ b/packages/block-editor/src/components/media-placeholder/styles.native.scss @@ -19,6 +19,10 @@ background-color: $background-dark-secondary; } +.emptyStateContainerDark { + background-color: $background-dark-secondary; +} + .emptyStateTitle { text-align: center; margin-top: 8; diff --git a/packages/block-editor/src/components/media-upload-progress/index.native.js b/packages/block-editor/src/components/media-upload-progress/index.native.js index 18856fb596606f..ca13038a2795ed 100644 --- a/packages/block-editor/src/components/media-upload-progress/index.native.js +++ b/packages/block-editor/src/components/media-upload-progress/index.native.js @@ -123,7 +123,11 @@ export class MediaUploadProgress extends React.Component { return ( - { showSpinner && } + { showSpinner && + + + + } { coverUrl && { ( sizes ) => { diff --git a/packages/block-editor/src/components/media-upload-progress/styles.native.scss b/packages/block-editor/src/components/media-upload-progress/styles.native.scss index 5dea1caaf5aeff..9924af03a33c42 100644 --- a/packages/block-editor/src/components/media-upload-progress/styles.native.scss +++ b/packages/block-editor/src/components/media-upload-progress/styles.native.scss @@ -1,4 +1,7 @@ .mediaUploadProgress { flex: 1; +} + +.progressBar { background-color: $gray-lighten-30; } diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index 8bed24e0928909..3d89091e94da3d 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -85,7 +85,7 @@ export class MediaUpload extends React.Component { onPickerSelect( requestFunction ) { const { allowedTypes = [], onSelect, multiple = false } = this.props; requestFunction( allowedTypes, multiple, ( media ) => { - if ( ( multiple && media ) || media.id ) { + if ( ( multiple && media ) || ( media && media.id ) ) { onSelect( media ); } } ); diff --git a/packages/block-library/src/video/gridicon-play.native.js b/packages/block-editor/src/components/video-player/gridicon-play.native.js similarity index 100% rename from packages/block-library/src/video/gridicon-play.native.js rename to packages/block-editor/src/components/video-player/gridicon-play.native.js diff --git a/packages/block-library/src/video/video-player.native.js b/packages/block-editor/src/components/video-player/index.native.js similarity index 96% rename from packages/block-library/src/video/video-player.native.js rename to packages/block-editor/src/components/video-player/index.native.js index 31c2cdb7b64baa..73c2038a0b457c 100644 --- a/packages/block-library/src/video/video-player.native.js +++ b/packages/block-editor/src/components/video-player/index.native.js @@ -14,9 +14,12 @@ import { default as VideoPlayer } from 'react-native-video'; /** * Internal dependencies */ -import styles from './video-player.scss'; +import styles from './styles.scss'; import PlayIcon from './gridicon-play'; +// Default Video ratio 16:9 +export const VIDEO_ASPECT_RATIO = 16 / 9; + class Video extends Component { constructor() { super( ...arguments ); diff --git a/packages/block-library/src/video/video-player.native.scss b/packages/block-editor/src/components/video-player/styles.native.scss similarity index 100% rename from packages/block-library/src/video/video-player.native.scss rename to packages/block-editor/src/components/video-player/styles.native.scss diff --git a/packages/block-library/src/media-text/edit.native.js b/packages/block-library/src/media-text/edit.native.js index faf385e0ce34df..73532a322916ae 100644 --- a/packages/block-library/src/media-text/edit.native.js +++ b/packages/block-library/src/media-text/edit.native.js @@ -43,6 +43,7 @@ class MediaTextEdit extends Component { super( ...arguments ); this.onSelectMedia = this.onSelectMedia.bind( this ); + this.onMediaUpdate = this.onMediaUpdate.bind( this ); this.onWidthChange = this.onWidthChange.bind( this ); this.commitWidthChange = this.commitWidthChange.bind( this ); this.state = { @@ -68,7 +69,7 @@ class MediaTextEdit extends Component { mediaType = media.type; } - if ( mediaType === 'image' ) { + if ( mediaType === 'image' && media.sizes ) { // Try the "large" size URL, falling back to the "full" size URL below. src = get( media, [ 'sizes', 'large', 'url' ] ) || get( media, [ 'media_details', 'sizes', 'large', 'source_url' ] ); } @@ -83,6 +84,15 @@ class MediaTextEdit extends Component { } ); } + onMediaUpdate( media ) { + const { setAttributes } = this.props; + + setAttributes( { + mediaId: media.id, + mediaUrl: media.url, + } ); + } + onWidthChange( width ) { this.setState( { mediaWidth: applyWidthConstraints( width ), @@ -101,16 +111,17 @@ class MediaTextEdit extends Component { } renderMediaArea() { - const { attributes } = this.props; + const { attributes, isSelected } = this.props; const { mediaAlt, mediaId, mediaPosition, mediaType, mediaUrl, mediaWidth, imageFill, focalPoint } = attributes; return ( ); } diff --git a/packages/block-library/src/media-text/icon-retry.native.js b/packages/block-library/src/media-text/icon-retry.native.js new file mode 100644 index 00000000000000..bdd40f69efd64a --- /dev/null +++ b/packages/block-library/src/media-text/icon-retry.native.js @@ -0,0 +1,6 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/components'; + +export default ; diff --git a/packages/block-library/src/media-text/media-container.native.js b/packages/block-library/src/media-text/media-container.native.js index 0b47812e5f5ac0..a647247c09bdc6 100644 --- a/packages/block-library/src/media-text/media-container.native.js +++ b/packages/block-library/src/media-text/media-container.native.js @@ -1,51 +1,82 @@ /** * External dependencies */ -import { View, Image, ImageBackground } from 'react-native'; +import { View, ImageBackground, Text, TouchableWithoutFeedback } from 'react-native'; +import { + requestMediaImport, + mediaUploadSync, + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, +} from 'react-native-gutenberg-bridge'; /** * WordPress dependencies */ -import { IconButton, Toolbar, withNotices } from '@wordpress/components'; +import { + Icon, + IconButton, + Toolbar, + withNotices, +} from '@wordpress/components'; import { BlockControls, BlockIcon, - MediaPlaceholder, MEDIA_TYPE_IMAGE, + MEDIA_TYPE_VIDEO, + MediaPlaceholder, MediaUpload, + MediaUploadProgress, + VIDEO_ASPECT_RATIO, + VideoPlayer, } from '@wordpress/block-editor'; import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { isURL } from '@wordpress/url'; /** * Internal dependencies */ +import styles from './style.scss'; import icon from './media-container-icon'; +import SvgIconRetry from './icon-retry'; -export function calculatePreferedImageSize( image, container ) { - const maxWidth = container.clientWidth; - const exceedMaxWidth = image.width > maxWidth; - const ratio = image.height / image.width; - const width = exceedMaxWidth ? maxWidth : image.width; - const height = exceedMaxWidth ? maxWidth * ratio : image.height; - return { width, height }; -} +/** + * Constants + */ +const ALLOWED_MEDIA_TYPES = [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ]; class MediaContainer extends Component { constructor() { super( ...arguments ); this.onUploadError = this.onUploadError.bind( this ); - this.calculateSize = this.calculateSize.bind( this ); - this.onLayout = this.onLayout.bind( this ); + this.updateMediaProgress = this.updateMediaProgress.bind( this ); + this.finishMediaUploadWithSuccess = this.finishMediaUploadWithSuccess.bind( this ); + this.finishMediaUploadWithFailure = this.finishMediaUploadWithFailure.bind( this ); + this.mediaUploadStateReset = this.mediaUploadStateReset.bind( this ); this.onSelectMediaUploadOption = this.onSelectMediaUploadOption.bind( this ); + this.onMediaPressed = this.onMediaPressed.bind( this ); this.state = { - width: 0, - height: 0, + isUploadInProgress: false, }; + } + + componentDidMount() { + const { mediaId, mediaUrl, onSelectMedia } = this.props; - if ( this.props.mediaUrl ) { - this.onMediaChange(); + if ( mediaId && mediaUrl && ! isURL( mediaUrl ) ) { + if ( mediaUrl.indexOf( 'file:' ) === 0 ) { + requestMediaImport( mediaUrl, ( id, url, type ) => { + if ( url ) { + onSelectMedia( { + media_type: type, + id, + url, + } ); + } + } ); + } + mediaUploadSync(); } } @@ -55,107 +86,171 @@ class MediaContainer extends Component { noticeOperations.createErrorNotice( message ); } - onSelectMediaUploadOption( { id, url } ) { + onSelectMediaUploadOption( params ) { + const { id, url, type } = params; const { onSelectMedia } = this.props; onSelectMedia( { - media_type: 'image', + media_type: type, id, url, } ); } - renderToolbarEditButton() { - const { mediaId } = this.props; + onMediaPressed() { + const { mediaId, mediaUrl } = this.props; + + if ( this.state.isUploadInProgress ) { + requestImageUploadCancelDialog( mediaId ); + } else if ( mediaId && ! isURL( mediaUrl ) ) { + requestImageFailedRetryDialog( mediaId ); + } + } + + getIcon( isRetryIcon, isVideo ) { + if ( isRetryIcon ) { + return ; + } + + return ; + } + + renderToolbarEditButton( open ) { return ( - ( - - ) } + ); } - componentDidUpdate( prevProps ) { - if ( prevProps.mediaUrl !== this.props.mediaUrl ) { - this.onMediaChange(); + updateMediaProgress() { + if ( ! this.state.isUploadInProgress ) { + this.setState( { isUploadInProgress: true } ); } } - onMediaChange() { - const mediaType = this.props.mediaType; - if ( mediaType === 'video' ) { + finishMediaUploadWithSuccess( payload ) { + const { onMediaUpdate } = this.props; - } else if ( mediaType === 'image' ) { - Image.getSize( this.props.mediaUrl, ( width, height ) => { - this.media = { width, height }; - this.calculateSize(); - } ); - } + onMediaUpdate( { + id: payload.mediaServerId, + url: payload.mediaUrl, + } ); + this.setState( { isUploadInProgress: false } ); } - calculateSize() { - if ( this.media === undefined || this.container === undefined ) { - return; - } - - const { width, height } = calculatePreferedImageSize( this.media, this.container ); - this.setState( { width, height } ); + finishMediaUploadWithFailure() { + this.setState( { isUploadInProgress: false } ); } - onLayout( event ) { - const { width, height } = event.nativeEvent.layout; - this.container = { - clientWidth: width, - clientHeight: height, - }; - this.calculateSize(); + mediaUploadStateReset() { + const { onMediaUpdate } = this.props; + + onMediaUpdate( { id: null, url: null } ); + this.setState( { isUploadInProgress: false } ); } - renderImage() { - const { mediaAlt, mediaUrl } = this.props; + renderImage( params, openMediaOptions ) { + const { isUploadInProgress } = this.state; + const { mediaAlt, mediaUrl, isSelected } = this.props; + const { finalWidth, finalHeight, imageWidthWithinContainer, isUploadFailed, retryMessage } = params; + const opacity = isUploadInProgress ? 0.3 : 1; return ( - - - - + + + { ! imageWidthWithinContainer && + + { this.getIcon( false ) } + } + + { isUploadFailed && + + + { this.getIcon( isUploadFailed ) } + + { retryMessage } + + } + + + ); } - renderVideo() { - const style = { videoContainer: {} }; + renderVideo( params, openMediaOptions ) { + const { mediaUrl, isSelected } = this.props; + const { isUploadInProgress } = this.state; + const { isUploadFailed, retryMessage } = params; + const showVideo = isURL( mediaUrl ) && ! isUploadInProgress && ! isUploadFailed; + return ( - - - { /* TODO: show video preview */ } + + + { showVideo && + + + + } + { ! showVideo && + + + { isUploadFailed ? this.getIcon( isUploadFailed ) : this.getIcon( false ) } + + { isUploadFailed && { retryMessage } } + + } - + ); } + renderContent( params, openMediaOptions ) { + const { mediaType } = this.props; + let mediaElement = null; + + switch ( mediaType ) { + case MEDIA_TYPE_IMAGE: + mediaElement = this.renderImage( params, openMediaOptions ); + break; + case MEDIA_TYPE_VIDEO: + mediaElement = this.renderVideo( params, openMediaOptions ); + break; + } + return mediaElement; + } + renderPlaceholder() { return ( ); } render() { - const { mediaUrl, mediaType } = this.props; - if ( mediaType && mediaUrl ) { - let mediaElement = null; - switch ( mediaType ) { - case 'image': - mediaElement = this.renderImage(); - break; - case 'video': - mediaElement = this.renderVideo(); - break; - } - return mediaElement; + const { mediaUrl, mediaId, mediaType } = this.props; + const coverUrl = mediaType === MEDIA_TYPE_IMAGE ? mediaUrl : null; + + if ( mediaUrl ) { + return ( + + { + return <> + { getMediaOptions() } + { this.renderToolbarEditButton( open ) } + + { + return ( + + { this.renderContent( params, open ) } + + ); + } } + /> + ; + } } + /> + + ); } return this.renderPlaceholder(); } diff --git a/packages/block-library/src/media-text/style.native.scss b/packages/block-library/src/media-text/style.native.scss index f1c3550f29c1e9..9d7e55d498caee 100644 --- a/packages/block-library/src/media-text/style.native.scss +++ b/packages/block-library/src/media-text/style.native.scss @@ -27,3 +27,70 @@ .is-vertically-aligned-bottom { align-items: flex-end; } + +.content { + flex: 1; +} + +.imageContainer { + align-items: center; + background-color: $gray-lighten-30; + height: 144; + justify-content: center; +} + +.modalIcon { + width: 24px; + height: 24px; + justify-content: center; + align-items: center; +} + +.icon { + fill: $gray-dark; + width: 24px; + height: 24px; +} + +.iconRetry { + fill: #fff; + width: 100%; + height: 100%; +} + +.iconRetryVideo { + fill: $gray-dark; +} + +.video { + width: 100%; + height: 100%; +} + +.videoContainer { + flex: 1; + background-color: #000; +} + +.videoPlaceholder { + width: 100%; + height: 100%; + align-items: center; + background-color: $gray-lighten-30; + justify-content: center; +} + +.uploadFailed { + background-color: rgba(0, 0, 0, 0.5); + flex: 1; +} + +.uploadFailedText { + color: #fff; + font-size: 14; + margin-top: 5; +} + +.uploadFailedTextVideo { + color: $gray-dark; +} diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 1a193a521e7032..bec2dfa7143e86 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -6,7 +6,6 @@ import { View, TouchableWithoutFeedback, Text } from 'react-native'; /** * Internal dependencies */ -import Video from './video-player'; import { mediaUploadSync, requestImageFailedRetryDialog, @@ -29,6 +28,8 @@ import { MediaUploadProgress, MEDIA_TYPE_VIDEO, BlockControls, + VIDEO_ASPECT_RATIO, + VideoPlayer, } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { isURL } from '@wordpress/url'; @@ -41,8 +42,6 @@ import style from './style.scss'; import SvgIcon from './icon'; import SvgIconRetry from './icon-retry'; -const VIDEO_ASPECT_RATIO = 1.7; - class VideoEdit extends React.Component { constructor( props ) { super( props ); @@ -225,7 +224,7 @@ class VideoEdit extends React.Component { { showVideo && -