diff --git a/server/perkeepd/ui/audio_detail.css b/server/perkeepd/ui/audio_detail.css
new file mode 100644
index 000000000..4581c6ac8
--- /dev/null
+++ b/server/perkeepd/ui/audio_detail.css
@@ -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;
+ }
+ }
+ }
+}
diff --git a/server/perkeepd/ui/audio_detail.js b/server/perkeepd/ui/audio_detail.js
new file mode 100644
index 000000000..026551db0
--- /dev/null
+++ b/server/perkeepd/ui/audio_detail.js
@@ -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;
+ }
+};
diff --git a/server/perkeepd/ui/index.html b/server/perkeepd/ui/index.html
index 8c1772604..7df08088b 100644
--- a/server/perkeepd/ui/index.html
+++ b/server/perkeepd/ui/index.html
@@ -54,6 +54,7 @@ limitations under the License.
+
diff --git a/server/perkeepd/ui/index.js b/server/perkeepd/ui/index.js
index 0193c7701..065fc52bc 100644
--- a/server/perkeepd/ui/index.js
+++ b/server/perkeepd/ui/index.js
@@ -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) {