mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-24 23:48:13 -05:00
Added basic version for audio card UI
refs https://github.com/TryGhost/Team/issues/1230 - adds basic audio card edit mode UI with upload functionality
This commit is contained in:
parent
5f9d091e9a
commit
a45e345a95
2 changed files with 349 additions and 6 deletions
|
@ -1 +1,121 @@
|
||||||
{{yield}}
|
<KoenigCard
|
||||||
|
@env={{@env}}
|
||||||
|
@class={{concat (kg-style "container-card") " kg-video-card mih10 miw-100 relative" (kg-style "breakout" size=@payload.cardWidth)}}
|
||||||
|
@headerOffset={{@headerOffset}}
|
||||||
|
@toolbar={{this.toolbar}}
|
||||||
|
@payload={{@payload}}
|
||||||
|
@isSelected={{@isSelected}}
|
||||||
|
@isEditing={{@isEditing}}
|
||||||
|
@selectCard={{@selectCard}}
|
||||||
|
@deselectCard={{@deselectCard}}
|
||||||
|
@editCard={{@editCard}}
|
||||||
|
@hasEditMode={{not this.isEmpty}}
|
||||||
|
@saveCard={{@saveCard}}
|
||||||
|
@saveAsSnippet={{@saveAsSnippet}}
|
||||||
|
@addParagraphAfterCard={{@addParagraphAfterCard}}
|
||||||
|
@moveCursorToPrevSection={{@moveCursorToPrevSection}}
|
||||||
|
@moveCursorToNextSection={{@moveCursorToNextSection}}
|
||||||
|
@editor={{@editor}}
|
||||||
|
{{did-insert this.didInsert}}
|
||||||
|
{{on "dragover" this.dragOver}}
|
||||||
|
{{on "dragleave" this.dragLeave}}
|
||||||
|
{{on "drop" this.drop}}
|
||||||
|
as |card|
|
||||||
|
>
|
||||||
|
<div style="display:flex">
|
||||||
|
<div style="width: 150px;">
|
||||||
|
<GhUploader
|
||||||
|
@uploadUrl="/media/upload/"
|
||||||
|
@resourceName="media"
|
||||||
|
@files={{this.files}}
|
||||||
|
@accept={{this.audioMimeTypes}}
|
||||||
|
@extensions={{this.audioExtensions}}
|
||||||
|
@onStart={{this.audioUploadStarted}}
|
||||||
|
@onComplete={{this.audioUploadCompleted}}
|
||||||
|
@onFailed={{this.audioUploadFailed}}
|
||||||
|
as |uploader|
|
||||||
|
>
|
||||||
|
<div class="relative{{unless (or this.previewThumbnailSrc @payload.thumbnailSrc) " bg-whitegrey-l2"}}">
|
||||||
|
{{#if (or this.previewThumbnailSrc @payload.thumbnailSrc)}}
|
||||||
|
<img src={{or this.previewThumbnailSrc @payload.thumbnailSrc}} class="{{kg-style this.kgImgStyle}}">
|
||||||
|
{{#if this.isDraggedOver}}
|
||||||
|
<div class="absolute absolute--fill flex items-center bg-black-60 pe-none">
|
||||||
|
<span class="db center sans-serif fw7 f7 white">
|
||||||
|
Drop to replace audio
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (or uploader.errors uploader.isUploading (not @payload.src))}}
|
||||||
|
<div class="relative miw-100 flex items-center {{if (not this.previewThumbnailSrc @payload.src) "kg-media-placeholder ba b--whitegrey" "absolute absolute--fill bg-white-50"}}">
|
||||||
|
{{#if uploader.errors}}
|
||||||
|
<span class="db absolute top-0 right-0 left-0 flex items-center h8 pl2 pr2 bg-red white sans-serif f7">
|
||||||
|
{{uploader.errors.firstObject.message}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.isDraggedOver}}
|
||||||
|
<span class="db center sans-serif fw7 f7 middarkgrey">
|
||||||
|
Drop it like it's hot 🔥
|
||||||
|
</span>
|
||||||
|
{{else if uploader.isUploading}}
|
||||||
|
{{uploader.progressBar}}
|
||||||
|
{{else if (not this.previewThumbnailSrc @payload.src)}}
|
||||||
|
<button class="flex flex-column items-center center sans-serif fw4 f7 middarkgrey kg-image-button" {{on "click" this.triggerAudioFileDialog}}>
|
||||||
|
{{svg-jar this.placeholder class="kg-placeholder-image" style="transform:scale(0.5)"}}
|
||||||
|
{{!-- <span class="mt2 midgrey">Click to select an audio</span> --}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="relative miw-100 flex items-center {{if (not this.previewThumbnailSrc @payload.src) "kg-media-placeholder ba b--whitegrey" "absolute absolute--fill bg-white-50"}}">
|
||||||
|
{{#if this.isDraggedOver}}
|
||||||
|
<span class="db center sans-serif fw7 f7 middarkgrey">
|
||||||
|
Drop it like it's hot 🔥
|
||||||
|
</span>
|
||||||
|
{{else if uploader.isUploading}}
|
||||||
|
{{uploader.progressBar}}
|
||||||
|
{{else if (not this.previewThumbnailSrc @payload.src)}}
|
||||||
|
<button class="flex flex-column items-center center sans-serif fw4 f7 middarkgrey kg-image-button" {{on "click" this.triggerAudioFileDialog}}>
|
||||||
|
{{svg-jar this.placeholder class="kg-placeholder-image" style="transform:scale(0.5)"}}
|
||||||
|
{{!-- <span class="mt2 midgrey">Click to select an audio</span> --}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:none">
|
||||||
|
<GhFileInput @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.audioMimeTypes}} @onInsert={{this.registerAudioFileInput}} />
|
||||||
|
</div>
|
||||||
|
</GhUploader>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div style="flex-grow: 1;display: flex;align-items: center;margin-left: 12px">
|
||||||
|
Click to upload an audio file
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{!-- {{#if (or @isSelected (clean-basic-html @payload.caption))}}
|
||||||
|
<card.CaptionInput
|
||||||
|
@caption={{@payload.caption}}
|
||||||
|
@update={{fn this.updatePayloadAttr "caption"}}
|
||||||
|
@placeholder="Type caption for audio (optional)" />
|
||||||
|
{{/if}} --}}
|
||||||
|
|
||||||
|
{{#if (and @isEditing @payload.src)}}
|
||||||
|
<KoenigSettingsPanel>
|
||||||
|
<div class="kg-settings-panel-control kg-settings-panel-control-horizontal">
|
||||||
|
<label class="kg-settings-panel-control-label" for="loop-toggle">Loop</label>
|
||||||
|
<div class="kg-settings-panel-control-input">
|
||||||
|
<div class="for-switch x-small">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" class="gh-input" id="loop-toggle" checked={{@payload.loop}} {{on "input" this.toggleLoop}}>
|
||||||
|
<span class="input-toggle-component mt1"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</KoenigSettingsPanel>
|
||||||
|
{{/if}}
|
||||||
|
</KoenigCard>
|
||||||
|
|
|
@ -1,10 +1,233 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
|
import {TrackedObject} from 'tracked-built-ins';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {bind} from '@ember/runloop';
|
||||||
|
import {guidFor} from '@ember/object/internals';
|
||||||
|
import {isBlank} from '@ember/utils';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
import {set} from '@ember/object';
|
||||||
|
import {task} from 'ember-concurrency-decorators';
|
||||||
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export const AUDIO_EXTENSIONS = ['mp4', 'mp3', 'wav'];
|
||||||
|
export const AUDIO_MIME_TYPES = ['audio/mp4', 'audio/mpeg', 'audio/ogg'];
|
||||||
|
|
||||||
|
const PLACEHOLDERS = ['summer', 'mountains', 'ufo-attack'];
|
||||||
|
|
||||||
|
/* Payload
|
||||||
|
{
|
||||||
|
src: 'https://ghostsite.com/media/...',
|
||||||
|
fileName: '...',
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
duration: 60,
|
||||||
|
mimeType: 'audio/mp4'
|
||||||
|
thumbnailSrc: 'https://ghostsite.com/images/...',
|
||||||
|
thumbnailWidth: 640,
|
||||||
|
thumbnailHeight: 640,
|
||||||
|
cardWidth: 'normal|wide|full',
|
||||||
|
loop: true|false (default: false)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: query file size limit from config and forbid uploads before they start
|
||||||
|
|
||||||
export default class KoenigCardAudioComponent extends Component {
|
export default class KoenigCardAudioComponent extends Component {
|
||||||
@service config;
|
@service ajax;
|
||||||
@service feature;
|
@service ghostPaths;
|
||||||
@service store;
|
|
||||||
@service membersUtils;
|
@tracked files;
|
||||||
@service ui;
|
@tracked isDraggedOver = false;
|
||||||
|
@tracked previewThumbnailSrc;
|
||||||
|
|
||||||
|
// previewPayload stores all of the data collected until upload completes
|
||||||
|
// at which point it will be saved to the real payload and the preview deleted
|
||||||
|
@tracked previewPayload = new TrackedObject({});
|
||||||
|
|
||||||
|
audioExtensions = AUDIO_EXTENSIONS;
|
||||||
|
audioMimeTypes = AUDIO_MIME_TYPES;
|
||||||
|
placeholder = PLACEHOLDERS[Math.floor(Math.random() * PLACEHOLDERS.length)]
|
||||||
|
|
||||||
|
payloadAudioAttrs = ['src', 'fileName', 'width', 'height', 'duration', 'mimeType', 'thumbnailSrc', 'thumbnailWidth', 'thumbnailHeight'];
|
||||||
|
|
||||||
|
get isEmpty() {
|
||||||
|
return isBlank(this.args.payload.src);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isIncomplete() {
|
||||||
|
const {src, thumbnailSrc} = this.args.payload;
|
||||||
|
return isBlank(src) || isBlank(thumbnailSrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
get toolbar() {
|
||||||
|
if (this.args.isEditing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [{
|
||||||
|
buttonClass: 'fw4 flex items-center white',
|
||||||
|
icon: 'koenig/kg-edit',
|
||||||
|
iconClass: 'fill-white',
|
||||||
|
title: 'Edit',
|
||||||
|
text: '',
|
||||||
|
action: bind(this, this.args.editCard)
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.args.registerComponent(this);
|
||||||
|
|
||||||
|
const payloadDefaults = {
|
||||||
|
loop: false
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(payloadDefaults).forEach(([key, value]) => {
|
||||||
|
if (this.args.payload[key] === undefined) {
|
||||||
|
this.updatePayloadAttr(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
didInsert(element) {
|
||||||
|
// required for snippet rects to be calculated - editor reaches in to component,
|
||||||
|
// expecting a non-Glimmer component with a .element property
|
||||||
|
this.element = element;
|
||||||
|
|
||||||
|
const {triggerBrowse, src, files} = this.args.payload;
|
||||||
|
|
||||||
|
// don't persist editor-only payload attrs
|
||||||
|
delete this.args.payload.triggerBrowse;
|
||||||
|
delete this.args.payload.files;
|
||||||
|
|
||||||
|
// the editor will add a triggerBrowse payload attr when inserting from
|
||||||
|
// the card menu to save an extra click needed to open the file dialog
|
||||||
|
if (triggerBrowse && !src && !files) {
|
||||||
|
this.triggerAudioFileDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// payload.files will be present if we have an externally set audio that
|
||||||
|
// should be uploaded. Typically from a paste or drag/drop
|
||||||
|
if (files) {
|
||||||
|
this.files = files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
registerAudioFileInput(input) {
|
||||||
|
this._audioFileInput = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
triggerAudioFileDialog(event) {
|
||||||
|
if (this._audioFileInput) {
|
||||||
|
return this._audioFileInput.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event?.target || this.element;
|
||||||
|
|
||||||
|
const cardElem = target.closest('.__mobiledoc-card');
|
||||||
|
const fileInput = cardElem?.querySelector('input[type="file"]');
|
||||||
|
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async audioUploadStarted() {
|
||||||
|
// TODO: Placeholder for any processing on audio upload
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async audioUploadCompleted([audio]) {
|
||||||
|
this.previewPayload.src = audio.url;
|
||||||
|
this.previewPayload.fileName = audio.fileName;
|
||||||
|
|
||||||
|
// save preview payload attrs into actual payload and create undo snapshot
|
||||||
|
this.args.editor.run(() => {
|
||||||
|
this.payloadAudioAttrs.forEach((attr) => {
|
||||||
|
this.updatePayloadAttr(attr, this.previewPayload[attr]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// reset preview so we're back to rendering saved data
|
||||||
|
this.previewPayload = new TrackedObject({});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
audioUploadFailed() {
|
||||||
|
// reset all attrs, creating an undo snapshot
|
||||||
|
this.args.editor.run(() => {
|
||||||
|
this.payloadAudioAttrs.forEach((attr) => {
|
||||||
|
this.updatePayloadAttr(attr, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@task
|
||||||
|
*uploadThumbnailFromBlobTask(audioUrl, fileBlob) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileBlob, `media-thumbnail-${guidFor(this)}.jpg`);
|
||||||
|
formData.append('url', audioUrl);
|
||||||
|
|
||||||
|
const url = `${this.ghostPaths.apiRoot}/media/thumbnail/upload/`;
|
||||||
|
|
||||||
|
const response = yield this.ajax.put(url, {
|
||||||
|
data: formData,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
dataType: 'json'
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.media[0].url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleLoop() {
|
||||||
|
this.updatePayloadAttr('loop', !this.args.payload.loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updatePayloadAttr(attr, value) {
|
||||||
|
const {payload} = this.args;
|
||||||
|
|
||||||
|
set(payload, attr, value);
|
||||||
|
|
||||||
|
// update the mobiledoc and stay in edit mode
|
||||||
|
this.args.saveCard(payload, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
dragOver(event) {
|
||||||
|
if (!event.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.isDraggedOver = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
dragLeave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.isDraggedOver = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
drop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
this.isDraggedOver = false;
|
||||||
|
|
||||||
|
if (event.dataTransfer.files) {
|
||||||
|
this.files = [event.dataTransfer.files[0]];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue