diff options
author | Eugen Rochko <eugen@zeonfederated.com> | 2019-08-15 15:13:26 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-15 15:13:26 +0200 |
commit | 28636f43e4b0c04befa243b847c38e81c90f1289 (patch) | |
tree | c0c2210eb09026e02dde629cdb6899893c360437 /app/javascript | |
parent | f178a01c11a4af077926dd035a0c4c44f6b3985c (diff) |
Add OCR tool to media editing modal (#11566)
Diffstat (limited to 'app/javascript')
4 files changed, 125 insertions, 28 deletions
diff --git a/app/javascript/mastodon/features/compose/components/upload_form.js b/app/javascript/mastodon/features/compose/components/upload_form.js index 9ff2aa0fa..c6eac554e 100644 --- a/app/javascript/mastodon/features/compose/components/upload_form.js +++ b/app/javascript/mastodon/features/compose/components/upload_form.js @@ -4,6 +4,7 @@ import UploadProgressContainer from '../containers/upload_progress_container'; import ImmutablePureComponent from 'react-immutable-pure-component'; import UploadContainer from '../containers/upload_container'; import SensitiveButtonContainer from '../containers/sensitive_button_container'; +import { FormattedMessage } from 'react-intl'; export default class UploadForm extends ImmutablePureComponent { @@ -16,7 +17,7 @@ export default class UploadForm extends ImmutablePureComponent { return ( <div className='compose-form__upload-wrapper'> - <UploadProgressContainer /> + <UploadProgressContainer icon='upload' message={<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />} /> <div className='compose-form__uploads-wrapper'> {mediaIds.map(id => ( diff --git a/app/javascript/mastodon/features/compose/components/upload_progress.js b/app/javascript/mastodon/features/compose/components/upload_progress.js index cbe58f573..b0bfe0c9a 100644 --- a/app/javascript/mastodon/features/compose/components/upload_progress.js +++ b/app/javascript/mastodon/features/compose/components/upload_progress.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import Motion from '../../ui/util/optional_motion'; import spring from 'react-motion/lib/spring'; -import { FormattedMessage } from 'react-intl'; import Icon from 'mastodon/components/icon'; export default class UploadProgress extends React.PureComponent { @@ -10,10 +9,12 @@ export default class UploadProgress extends React.PureComponent { static propTypes = { active: PropTypes.bool, progress: PropTypes.number, + icon: PropTypes.string.isRequired, + message: PropTypes.node.isRequired, }; render () { - const { active, progress } = this.props; + const { active, progress, icon, message } = this.props; if (!active) { return null; @@ -22,11 +23,11 @@ export default class UploadProgress extends React.PureComponent { return ( <div className='upload-progress'> <div className='upload-progress__icon'> - <Icon id='upload' /> + <Icon id={icon} /> </div> <div className='upload-progress__message'> - <FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' /> + {message} <div className='upload-progress__backdrop'> <Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}> diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index e15bf69d6..b21d5c9d4 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -10,6 +10,11 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import IconButton from 'mastodon/components/icon_button'; import Button from 'mastodon/components/button'; import Video from 'mastodon/features/video'; +import { TesseractWorker } from 'tesseract.js'; +import Textarea from 'react-textarea-autosize'; +import UploadProgress from 'mastodon/features/compose/components/upload_progress'; +import CharacterCounter from 'mastodon/features/compose/components/character_counter'; +import { length } from 'stringz'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -29,6 +34,12 @@ const mapDispatchToProps = (dispatch, { id }) => ({ }); +const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') + .replace(/\n/g, ' ') + .replace(/\*\*\*\*\*\*/g, '\n\n'); + +const assetHost = process.env.CDN_HOST || ''; + export default @connect(mapStateToProps, mapDispatchToProps) @injectIntl class FocalPointModal extends ImmutablePureComponent { @@ -47,6 +58,7 @@ class FocalPointModal extends ImmutablePureComponent { dragging: false, description: '', dirty: false, + progress: 0, }; componentWillMount () { @@ -133,9 +145,27 @@ class FocalPointModal extends ImmutablePureComponent { this.node = c; } + handleTextDetection = () => { + const { media } = this.props; + + const worker = new TesseractWorker({ + workerPath: `${assetHost}/packs/ocr/worker.min.js`, + corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`, + langPath: `${assetHost}/ocr/lang-data`, + }); + + this.setState({ detecting: true }); + + worker.recognize(media.get('url')) + .progress(({ progress }) => this.setState({ progress })) + .finally(() => worker.terminate()) + .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false })) + .catch(() => this.setState({ detecting: false })); + } + render () { const { media, intl, onClose } = this.props; - const { x, y, dragging, description, dirty } = this.state; + const { x, y, dragging, description, dirty, detecting, progress } = this.state; const width = media.getIn(['meta', 'original', 'width']) || null; const height = media.getIn(['meta', 'original', 'height']) || null; @@ -158,15 +188,27 @@ class FocalPointModal extends ImmutablePureComponent { <label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label> - <textarea - id='upload-modal__description' - className='setting-text light' - value={description} - onChange={this.handleChange} - autoFocus - /> + <div className='setting-text__wrapper'> + <Textarea + id='upload-modal__description' + className='setting-text light' + value={detecting ? '…' : description} + onChange={this.handleChange} + disabled={detecting} + autoFocus + /> + + <div className='setting-text__modifiers'> + <UploadProgress progress={progress * 100} active={detecting} icon='file-text-o' message={<FormattedMessage id='upload_modal.analyzing_picture' defaultMessage='Analyzing picture…' />} /> + </div> + </div> + + <div className='setting-text__toolbar'> + <button disabled={detecting || media.get('type') !== 'image'} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button> + <CharacterCounter max={420} text={detecting ? '' : description} /> + </div> - <Button disabled={!dirty} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> + <Button disabled={!dirty || detecting || length(description) > 420} text={intl.formatMessage(messages.apply)} onClick={this.handleSubmit} /> </div> <div className='report-modal__statuses'> diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f2967a398..893be9095 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3,6 +3,27 @@ -ms-overflow-style: -ms-autohiding-scrollbar; } +.link-button { + display: block; + font-size: 15px; + line-height: 20px; + color: $ui-highlight-color; + border: 0; + background: transparent; + padding: 0; + cursor: pointer; + + &:hover, + &:active { + text-decoration: underline; + } + + &:disabled { + color: $ui-primary-color; + cursor: default; + } +} + .button { background-color: $ui-highlight-color; border: 10px none; @@ -637,18 +658,6 @@ .character-counter__wrapper { align-self: center; margin-right: 4px; - - .character-counter { - cursor: default; - font-family: $font-sans-serif, sans-serif; - font-size: 14px; - font-weight: 600; - color: $lighter-text-color; - - &.character-counter--over { - color: $warning-red; - } - } } } @@ -665,6 +674,18 @@ } } +.character-counter { + cursor: default; + font-family: $font-sans-serif, sans-serif; + font-size: 14px; + font-weight: 600; + color: $lighter-text-color; + + &.character-counter--over { + color: $warning-red; + } +} + .no-reduce-motion .spoiler-input { transition: height 0.4s ease, opacity 0.4s ease; } @@ -4555,16 +4576,48 @@ a.status-card.compact:hover { padding: 10px; font-family: inherit; font-size: 14px; - resize: vertical; + resize: none; border: 0; outline: 0; border-radius: 4px; border: 1px solid $ui-secondary-color; - margin-bottom: 20px; + min-height: 100px; + max-height: 50vh; + margin-bottom: 10px; &:focus { border: 1px solid darken($ui-secondary-color, 8%); } + + &__wrapper { + background: $white; + border: 1px solid $ui-secondary-color; + margin-bottom: 10px; + border-radius: 4px; + + .setting-text { + border: 0; + margin-bottom: 0; + border-radius: 0; + + &:focus { + border: 0; + } + } + + &__modifiers { + color: $inverted-text-color; + font-family: inherit; + font-size: 14px; + background: $white; + } + } + + &__toolbar { + display: flex; + justify-content: space-between; + margin-bottom: 20px; + } } .setting-text-label { |