before. In the API, attachments now can be either image, video or gifv. Gifv is to be treated like images in terms of behaviour, but are videos by file type.master
@@ -75,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; | |||
export function fetchAccount(id) { | |||
return (dispatch, getState) => { | |||
dispatch(fetchRelationships([id])); | |||
if (getState().getIn(['accounts', id], null) !== null) { | |||
return; | |||
} | |||
dispatch(fetchAccountRequest(id)); | |||
api(getState).get(`/api/v1/accounts/${id}`).then(response => { | |||
dispatch(fetchAccountSuccess(response.data)); | |||
dispatch(fetchRelationships([id])); | |||
}).catch(error => { | |||
dispatch(fetchAccountFail(id, error)); | |||
}); | |||
@@ -0,0 +1,21 @@ | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
const ExtendedVideoPlayer = React.createClass({ | |||
propTypes: { | |||
src: React.PropTypes.string.isRequired | |||
}, | |||
mixins: [PureRenderMixin], | |||
render () { | |||
return ( | |||
<div> | |||
<video src={this.props.src} autoPlay muted loop /> | |||
</div> | |||
); | |||
}, | |||
}); | |||
export default ExtendedVideoPlayer; |
@@ -43,6 +43,141 @@ const spoilerButtonStyle = { | |||
zIndex: '100' | |||
}; | |||
const itemStyle = { | |||
boxSizing: 'border-box', | |||
position: 'relative', | |||
float: 'left', | |||
border: 'none', | |||
display: 'block' | |||
}; | |||
const thumbStyle = { | |||
display: 'block', | |||
width: '100%', | |||
height: '100%', | |||
textDecoration: 'none', | |||
backgroundSize: 'cover', | |||
cursor: 'zoom-in' | |||
}; | |||
const gifvThumbStyle = { | |||
position: 'relative', | |||
zIndex: '1', | |||
width: '100%', | |||
height: '100%', | |||
objectFit: 'cover', | |||
top: '50%', | |||
transform: 'translateY(-50%)', | |||
cursor: 'zoom-in' | |||
}; | |||
const Item = React.createClass({ | |||
propTypes: { | |||
attachment: ImmutablePropTypes.map.isRequired, | |||
index: React.PropTypes.number.isRequired, | |||
size: React.PropTypes.number.isRequired, | |||
onClick: React.PropTypes.func.isRequired | |||
}, | |||
mixins: [PureRenderMixin], | |||
handleClick (e) { | |||
const { index, onClick } = this.props; | |||
if (e.button === 0) { | |||
e.preventDefault(); | |||
onClick(index); | |||
} | |||
e.stopPropagation(); | |||
}, | |||
render () { | |||
const { attachment, index, size } = this.props; | |||
let width = 50; | |||
let height = 100; | |||
let top = 'auto'; | |||
let left = 'auto'; | |||
let bottom = 'auto'; | |||
let right = 'auto'; | |||
if (size === 1) { | |||
width = 100; | |||
} | |||
if (size === 4 || (size === 3 && index > 0)) { | |||
height = 50; | |||
} | |||
if (size === 2) { | |||
if (index === 0) { | |||
right = '2px'; | |||
} else { | |||
left = '2px'; | |||
} | |||
} else if (size === 3) { | |||
if (index === 0) { | |||
right = '2px'; | |||
} else if (index > 0) { | |||
left = '2px'; | |||
} | |||
if (index === 1) { | |||
bottom = '2px'; | |||
} else if (index > 1) { | |||
top = '2px'; | |||
} | |||
} else if (size === 4) { | |||
if (index === 0 || index === 2) { | |||
right = '2px'; | |||
} | |||
if (index === 1 || index === 3) { | |||
left = '2px'; | |||
} | |||
if (index < 2) { | |||
bottom = '2px'; | |||
} else { | |||
top = '2px'; | |||
} | |||
} | |||
let thumbnail = ''; | |||
if (attachment.get('type') === 'image') { | |||
thumbnail = ( | |||
<a | |||
href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} | |||
onClick={this.handleClick} | |||
target='_blank' | |||
style={{ background: `url(${attachment.get('preview_url')}) no-repeat center`, ...thumbStyle }} | |||
/> | |||
); | |||
} else if (attachment.get('type') === 'gifv') { | |||
thumbnail = ( | |||
<video | |||
src={attachment.get('url')} | |||
onClick={this.handleClick} | |||
autoPlay={true} | |||
loop={true} | |||
muted={true} | |||
style={gifvThumbStyle} | |||
/> | |||
); | |||
} | |||
return ( | |||
<div key={attachment.get('id')} style={{ ...itemStyle, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> | |||
{thumbnail} | |||
</div> | |||
); | |||
} | |||
}); | |||
const MediaGallery = React.createClass({ | |||
getInitialState () { | |||
@@ -61,17 +196,12 @@ const MediaGallery = React.createClass({ | |||
mixins: [PureRenderMixin], | |||
handleClick (index, e) { | |||
if (e.button === 0) { | |||
e.preventDefault(); | |||
this.props.onOpenMedia(this.props.media, index); | |||
} | |||
e.stopPropagation(); | |||
handleOpen (e) { | |||
this.setState({ visible: !this.state.visible }); | |||
}, | |||
handleOpen () { | |||
this.setState({ visible: !this.state.visible }); | |||
handleClick (index) { | |||
this.props.onOpenMedia(this.props.media, index); | |||
}, | |||
render () { | |||
@@ -80,87 +210,31 @@ const MediaGallery = React.createClass({ | |||
let children; | |||
if (!this.state.visible) { | |||
let warning; | |||
if (sensitive) { | |||
children = ( | |||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}> | |||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> | |||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> | |||
</div> | |||
); | |||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; | |||
} else { | |||
children = ( | |||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}> | |||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> | |||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> | |||
</div> | |||
); | |||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; | |||
} | |||
children = ( | |||
<div style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}> | |||
<span style={spoilerSpanStyle}>{warning}</span> | |||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> | |||
</div> | |||
); | |||
} else { | |||
const size = media.take(4).size; | |||
children = media.take(4).map((attachment, i) => { | |||
let width = 50; | |||
let height = 100; | |||
let top = 'auto'; | |||
let left = 'auto'; | |||
let bottom = 'auto'; | |||
let right = 'auto'; | |||
if (size === 1) { | |||
width = 100; | |||
} | |||
if (size === 4 || (size === 3 && i > 0)) { | |||
height = 50; | |||
} | |||
if (size === 2) { | |||
if (i === 0) { | |||
right = '2px'; | |||
} else { | |||
left = '2px'; | |||
} | |||
} else if (size === 3) { | |||
if (i === 0) { | |||
right = '2px'; | |||
} else if (i > 0) { | |||
left = '2px'; | |||
} | |||
if (i === 1) { | |||
bottom = '2px'; | |||
} else if (i > 1) { | |||
top = '2px'; | |||
} | |||
} else if (size === 4) { | |||
if (i === 0 || i === 2) { | |||
right = '2px'; | |||
} | |||
if (i === 1 || i === 3) { | |||
left = '2px'; | |||
} | |||
if (i < 2) { | |||
bottom = '2px'; | |||
} else { | |||
top = '2px'; | |||
} | |||
} | |||
return ( | |||
<div key={attachment.get('id')} style={{ boxSizing: 'border-box', position: 'relative', left: left, top: top, right: right, bottom: bottom, float: 'left', border: 'none', display: 'block', width: `${width}%`, height: `${height}%` }}> | |||
<a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, i)} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} /> | |||
</div> | |||
); | |||
}); | |||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />); | |||
} | |||
return ( | |||
<div style={{ ...outerStyle, height: `${this.props.height}px` }}> | |||
<div style={spoilerButtonStyle} > | |||
<div style={spoilerButtonStyle}> | |||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} /> | |||
</div> | |||
{children} | |||
</div> | |||
); | |||
@@ -74,8 +74,8 @@ const Status = React.createClass({ | |||
} | |||
if (status.get('media_attachments').size > 0 && !this.props.muted) { | |||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { | |||
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />; | |||
if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { | |||
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} autoplay={status.getIn(['media_attachments', 0, 'type']) === 'gifv'} sensitive={status.get('sensitive')} />; | |||
} else { | |||
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />; | |||
} | |||
@@ -175,7 +175,7 @@ const VideoPlayer = React.createClass({ | |||
); | |||
} else { | |||
return ( | |||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleOpen}> | |||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}> | |||
{spoilerButton} | |||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> | |||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> | |||
@@ -197,7 +197,7 @@ const VideoPlayer = React.createClass({ | |||
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}> | |||
{spoilerButton} | |||
{muteButton} | |||
<video ref={this.setRef} src={media.get('url')} autoPlay='true' loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} /> | |||
<video ref={this.setRef} src={media.get('url')} autoPlay={true} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} /> | |||
</div> | |||
); | |||
} | |||
@@ -38,7 +38,7 @@ const DetailedStatus = React.createClass({ | |||
let applicationLink = ''; | |||
if (status.get('media_attachments').size > 0) { | |||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { | |||
if (status.getIn(['media_attachments', 0, 'type']) === 'video' || (status.get('media_attachments').size === 1 && status.getIn(['media_attachments', 0, 'type']) === 'gifv')) { | |||
media = <VideoPlayer sensitive={status.get('sensitive')} media={status.getIn(['media_attachments', 0])} width={300} height={150} autoplay />; | |||
} else { | |||
media = <MediaGallery sensitive={status.get('sensitive')} media={status.get('media_attachments')} height={300} onOpenMedia={this.props.onOpenMedia} />; | |||
@@ -9,6 +9,7 @@ import ImageLoader from 'react-imageloader'; | |||
import LoadingIndicator from '../../../components/loading_indicator'; | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
import ExtendedVideoPlayer from '../../../components/extended_video_player'; | |||
const mapStateToProps = state => ({ | |||
media: state.getIn(['modal', 'media']), | |||
@@ -131,27 +132,34 @@ const Modal = React.createClass({ | |||
return null; | |||
} | |||
const url = media.get(index).get('url'); | |||
const attachment = media.get(index); | |||
const url = attachment.get('url'); | |||
let leftNav, rightNav; | |||
let leftNav, rightNav, content; | |||
leftNav = rightNav = ''; | |||
leftNav = rightNav = content = ''; | |||
if (media.size > 1) { | |||
leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; | |||
rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; | |||
} | |||
return ( | |||
<Lightbox {...other}> | |||
{leftNav} | |||
if (attachment.get('type') === 'image') { | |||
content = ( | |||
<ImageLoader | |||
src={url} | |||
preloader={preloader} | |||
imgProps={{ style: imageStyle }} | |||
/> | |||
); | |||
} else if (attachment.get('type') === 'gifv') { | |||
content = <ExtendedVideoPlayer src={url} />; | |||
} | |||
return ( | |||
<Lightbox {...other}> | |||
{leftNav} | |||
{content} | |||
{rightNav} | |||
</Lightbox> | |||
); | |||
@@ -104,8 +104,12 @@ | |||
overflow: hidden; | |||
width: 100%; | |||
box-sizing: border-box; | |||
height: 110px; | |||
display: flex; | |||
position: relative; | |||
.status__attachments__inner { | |||
display: flex; | |||
height: 214px; | |||
} | |||
} | |||
} | |||
@@ -184,8 +188,12 @@ | |||
overflow: hidden; | |||
width: 100%; | |||
box-sizing: border-box; | |||
height: 300px; | |||
display: flex; | |||
position: relative; | |||
.status__attachments__inner { | |||
display: flex; | |||
height: 360px; | |||
} | |||
} | |||
.video-player { | |||
@@ -231,11 +239,19 @@ | |||
text-decoration: none; | |||
cursor: zoom-in; | |||
} | |||
video { | |||
position: relative; | |||
z-index: 1; | |||
width: 100%; | |||
height: 100%; | |||
object-fit: cover; | |||
top: 50%; | |||
transform: translateY(-50%); | |||
} | |||
} | |||
.video-item { | |||
max-width: 196px; | |||
a { | |||
cursor: pointer; | |||
} | |||
@@ -258,6 +274,9 @@ | |||
width: 100%; | |||
height: 100%; | |||
cursor: pointer; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
@@ -1,15 +1,32 @@ | |||
# frozen_string_literal: true | |||
class MediaAttachment < ApplicationRecord | |||
self.inheritance_column = nil | |||
enum type: [:image, :gifv, :video] | |||
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze | |||
VIDEO_MIME_TYPES = ['video/webm', 'video/mp4'].freeze | |||
IMAGE_STYLES = { original: '1280x1280>', small: '400x400>' }.freeze | |||
VIDEO_STYLES = { | |||
small: { | |||
convert_options: { | |||
output: { | |||
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', | |||
}, | |||
}, | |||
format: 'png', | |||
time: 0, | |||
}, | |||
}.freeze | |||
belongs_to :account, inverse_of: :media_attachments | |||
belongs_to :status, inverse_of: :media_attachments | |||
has_attached_file :file, | |||
styles: -> (f) { file_styles f }, | |||
processors: -> (f) { f.video? ? [:transcoder] : [:thumbnail] }, | |||
styles: ->(f) { file_styles f }, | |||
processors: ->(f) { file_processors f }, | |||
convert_options: { all: '-quality 90 -strip' } | |||
validates_attachment_content_type :file, content_type: IMAGE_MIME_TYPES + VIDEO_MIME_TYPES | |||
validates_attachment_size :file, less_than: 8.megabytes | |||
@@ -27,45 +44,45 @@ class MediaAttachment < ApplicationRecord | |||
self.file = URI.parse(url) | |||
end | |||
def image? | |||
IMAGE_MIME_TYPES.include? file_content_type | |||
end | |||
def video? | |||
VIDEO_MIME_TYPES.include? file_content_type | |||
end | |||
def type | |||
image? ? 'image' : 'video' | |||
end | |||
def to_param | |||
shortcode | |||
end | |||
before_create :set_shortcode | |||
before_post_process :set_type | |||
class << self | |||
private | |||
def file_styles(f) | |||
if f.instance.image? | |||
if f.instance.file_content_type == 'image/gif' | |||
{ | |||
original: '1280x1280>', | |||
small: '400x400>', | |||
} | |||
else | |||
{ | |||
small: { | |||
small: IMAGE_STYLES[:small], | |||
original: { | |||
format: 'webm', | |||
convert_options: { | |||
output: { | |||
vf: 'scale=\'min(400\, iw):min(400\, ih)\':force_original_aspect_ratio=decrease', | |||
'c:v' => 'libvpx', | |||
'crf' => 6, | |||
'b:v' => '500K', | |||
}, | |||
}, | |||
format: 'png', | |||
time: 1, | |||
}, | |||
} | |||
elsif IMAGE_MIME_TYPES.include? f.instance.file_content_type | |||
IMAGE_STYLES | |||
else | |||
VIDEO_STYLES | |||
end | |||
end | |||
def file_processors(f) | |||
if f.file_content_type == 'image/gif' | |||
[:gif_transcoder] | |||
elsif VIDEO_MIME_TYPES.include? f.file_content_type | |||
[:transcoder] | |||
else | |||
[:thumbnail] | |||
end | |||
end | |||
end | |||
@@ -80,4 +97,8 @@ class MediaAttachment < ApplicationRecord | |||
break if MediaAttachment.find_by(shortcode: shortcode).nil? | |||
end | |||
end | |||
def set_type | |||
self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image | |||
end | |||
end |
@@ -1,5 +1,5 @@ | |||
object @media | |||
attribute :id, :type | |||
node(:url) { |media| full_asset_url(media.file.url( :original)) } | |||
node(:preview_url) { |media| full_asset_url(media.file.url( :small)) } | |||
node(:url) { |media| full_asset_url(media.file.url(:original)) } | |||
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } | |||
node(:text_url) { |media| medium_url(media) } |
@@ -22,9 +22,9 @@ | |||
.detailed-status__attachments | |||
- if status.sensitive? | |||
= render partial: 'stream_entries/content_spoiler' | |||
- status.media_attachments.each do |media| | |||
.media-item | |||
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" | |||
.status__attachments__inner | |||
- status.media_attachments.each do |media| | |||
= render partial: 'stream_entries/media', locals: { media: media } | |||
%div.detailed-status__meta | |||
%data.dt-published{ value: status.created_at.to_time.iso8601 } | |||
@@ -0,0 +1,4 @@ | |||
.media-item | |||
= link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : "", target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do | |||
- unless media.image? | |||
%video{ src: media.file.url(:original), autoplay: true, loop: true }/ |
@@ -22,11 +22,12 @@ | |||
- if status.sensitive? | |||
= render partial: 'stream_entries/content_spoiler' | |||
- if status.media_attachments.first.video? | |||
.video-item | |||
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do | |||
.video-item__play | |||
= fa_icon('play') | |||
.status__attachments__inner | |||
.video-item | |||
= link_to (status.media_attachments.first.remote_url.blank? ? status.media_attachments.first.file.url(:original) : status.media_attachments.first.remote_url), style: "background-image: url(#{status.media_attachments.first.file.url(:small)})", target: '_blank', rel: 'noopener', class: 'u-video' do | |||
.video-item__play | |||
= fa_icon('play') | |||
- else | |||
- status.media_attachments.each do |media| | |||
.media-item | |||
= link_to '', (media.remote_url.blank? ? media.file.url(:original) : media.remote_url), style: "background-image: url(#{media.file.url(:original)})", target: '_blank', rel: 'noopener', class: "u-#{media.video? ? 'video' : 'photo'}" | |||
.status__attachments__inner | |||
- status.media_attachments.each do |media| | |||
= render partial: 'stream_entries/media', locals: { media: media } |
@@ -2,12 +2,13 @@ require_relative 'boot' | |||
require 'rails/all' | |||
require_relative '../app/lib/exceptions' | |||
# Require the gems listed in Gemfile, including any gems | |||
# you've limited to :test, :development, or :production. | |||
Bundler.require(*Rails.groups) | |||
require_relative '../app/lib/exceptions' | |||
require_relative '../lib/paperclip/gif_transcoder' | |||
Dotenv::Railtie.load | |||
module Mastodon | |||
@@ -0,0 +1,12 @@ | |||
class AddTypeToMediaAttachments < ActiveRecord::Migration[5.0] | |||
def up | |||
add_column :media_attachments, :type, :integer, default: 0, null: false | |||
MediaAttachment.where(file_content_type: MediaAttachment::IMAGE_MIME_TYPES).update_all(type: MediaAttachment.types[:image]) | |||
MediaAttachment.where(file_content_type: MediaAttachment::VIDEO_MIME_TYPES).update_all(type: MediaAttachment.types[:video]) | |||
end | |||
def down | |||
remove_column :media_attachments, :type | |||
end | |||
end |
@@ -10,7 +10,7 @@ | |||
# | |||
# It's strongly recommended that you check this file into your version control system. | |||
ActiveRecord::Schema.define(version: 20170303212857) do | |||
ActiveRecord::Schema.define(version: 20170304202101) do | |||
# These are extensions that must be enabled in order to support this database | |||
enable_extension "plpgsql" | |||
@@ -98,6 +98,7 @@ ActiveRecord::Schema.define(version: 20170303212857) do | |||
t.datetime "created_at", null: false | |||
t.datetime "updated_at", null: false | |||
t.string "shortcode" | |||
t.integer "type", default: 0, null: false | |||
t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true, using: :btree | |||
t.index ["status_id"], name: "index_media_attachments_on_status_id", using: :btree | |||
end | |||
@@ -0,0 +1,21 @@ | |||
# frozen_string_literal: true | |||
module Paperclip | |||
# This transcoder is only to be used for the MediaAttachment model | |||
# to convert animated gifs to webm | |||
class GifTranscoder < Paperclip::Processor | |||
def make | |||
num_frames = identify('-format %n :file', file: file.path).to_i | |||
return file unless options[:style] == :original && num_frames > 1 | |||
final_file = Paperclip::Transcoder.make(file, options, attachment) | |||
attachment.instance.file_file_name = 'media.webm' | |||
attachment.instance.file_content_type = 'video/webm' | |||
attachment.instance.type = MediaAttachment.types[:gifv] | |||
final_file | |||
end | |||
end | |||
end |