server/perkeepd/ui: Implement blob detail audio

Summary:

Implement audio player using web ui.

Add new blob aspect for audio files.

Test Plan:

1. Upload browser compatible audio to perkeep
2. Navigate to web UI
3. Open the audio blob's detail page
4. Play audio

Before:

Audio cannot be played using web ui

After:

Compatible audio can be played and paused from the detail page

Audio title and artist are shown if available

Issue: #202
Change-Id: Icd450aaa16e9e622a677c23f4f1f699784453dbc
This commit is contained in:
Omar Chehab 2019-06-01 13:03:14 -07:00
parent a838f6e15a
commit 99f2e2dcb4
4 changed files with 164 additions and 3 deletions

View File

@ -0,0 +1,56 @@
/*
Copyright 2019 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.cam-detail-audio {
display: flex;
flex-direction: column;
width: 100%;
background: black;
&-controls {
display: flex;
align-items: center;
@media screen and (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
}
&-player {
padding: 12px;
font-size: 0;
flex-grow: 1;
audio {
width: 100%;
}
}
&-meta {
padding: 12px;
&-title {
color: white;
font-size: 17px;
}
&-artist {
color: lightgrey;
font-size: 15px;
}
}
}
}

View File

@ -0,0 +1,98 @@
/*
Copyright 2019 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
goog.provide('cam.AudioDetail');
goog.require('cam.BlobItemAudioContent');
cam.AudioDetail = React.createClass({
displayName: 'AudioDetail',
propTypes: {
permanodeMeta: React.PropTypes.object,
resolvedMeta: React.PropTypes.object.isRequired,
},
render: function() {
return React.DOM.div({
className: 'cam-detail-audio',
},
this.getControls_(),
);
},
getControls_: function() {
var mediaTags = this.props.resolvedMeta.mediaTags;
return React.DOM.div({
className: 'cam-detail-audio-controls',
},
React.DOM.div({
className: 'cam-detail-audio-controls-meta'
},
mediaTags.title != null && React.DOM.div({
className: 'cam-detail-audio-controls-meta-title',
}, mediaTags.title),
mediaTags.artist != null && React.DOM.div({
className: 'cam-detail-audio-controls-meta-artist',
}, mediaTags.artist),
),
React.DOM.div({
className: 'cam-detail-audio-controls-player',
},
React.DOM.audio({
controls: true,
key: this.props.resolvedMeta.blobRef,
src: goog.string.subs('%s%s/%s', goog.global.CAMLISTORE_CONFIG.downloadHelper, this.props.resolvedMeta.blobRef, this.props.resolvedMeta.file.fileName),
}),
),
);
},
});
cam.AudioDetail.getAspect = function(blobref, searchSession) {
if (!blobref) {
return null;
}
var rm = searchSession.getResolvedMeta(blobref);
var pm = searchSession.getMeta(blobref);
if (!pm) {
return null;
}
if (pm.camliType != 'permanode') {
pm = null;
}
if (rm && cam.BlobItemAudioContent.isAudio(rm)) {
return {
fragment: 'audio',
title: 'Audio',
createContent: function(size, backwardPiggy) {
return React.createElement(cam.AudioDetail, {
key: 'audio',
permanodeMeta: pm,
resolvedMeta: rm,
});
},
};
} else {
return null;
}
};

View File

@ -54,6 +54,7 @@ limitations under the License.
<link rel="stylesheet/less" href="index.css" type="text/css">
<link rel="stylesheet/less" href="header.css" type="text/css">
<link rel="stylesheet/less" href="detail.css" type="text/css">
<link rel="stylesheet/less" href="audio_detail.css" type="text/css">
<link rel="stylesheet/less" href="blob_item.css" type="text/css">
<link rel="stylesheet/less" href="blob_item_audio.css" type="text/css">

View File

@ -33,6 +33,8 @@ goog.require('goog.string');
goog.require('goog.Uri');
goog.require('cam.BlobDetail');
goog.require('cam.ImageDetail');
goog.require('cam.AudioDetail');
goog.require('cam.BlobItemContainerReact');
goog.require('cam.DirContainer');
goog.require('cam.BlobItemDemoContent');
@ -249,9 +251,10 @@ cam.IndexPage = React.createClass({
var specificAspects = [
cam.ImageDetail.getAspect,
cam.AudioDetail.getAspect,
cam.PdfDetail.getAspect,
this.getDirAspect_.bind(null),
].map(getAspect).filter(goog.functions.identity);
];
var generalAspects = [
this.getSearchAspect_.bind(null, specificAspects),
@ -260,9 +263,12 @@ cam.IndexPage = React.createClass({
this.props.availWidth, this.props.availHeight - this.HEADER_HEIGHT_,
this.updateSearchBarOnMap_, this.setPendingQuery_, this.childSearchSession_),
cam.BlobDetail.getAspect.bind(null, this.getDetailURL_, this.props.serverConnection),
].map(getAspect).filter(goog.functions.identity);
];
return specificAspects.concat(generalAspects);
return specificAspects
.concat(generalAspects)
.map(getAspect)
.filter(goog.functions.identity);
},
getSearchAspect_: function(specificAspects, blobref, targetSearchSession) {