Embedding HTML5 audio player for streaming within Iris, fixes #251

This commit is contained in:
James Barnsley
2018-02-01 08:52:15 +13:00
parent 8159991980
commit 168e9d7c7d
9 changed files with 559 additions and 239 deletions

View File

@ -49455,7 +49455,9 @@ var initialState = {
artists: {}, artists: {},
playlists: {}, playlists: {},
users: {}, users: {},
tracks: {} tracks: {},
http_streaming_enabled: false,
http_streaming_url: "http://" + window.location.hostname + ":8000/mopidy"
}, },
ui: { ui: {
show_initial_setup: true, show_initial_setup: true,
@ -58619,6 +58621,11 @@ var PlaybackControls = function (_React$Component) {
return _react2.default.createElement( return _react2.default.createElement(
'div', 'div',
{ className: this.state.expanded ? "expanded playback-controls" : "playback-controls" }, { className: this.state.expanded ? "expanded playback-controls" : "playback-controls" },
this.props.http_streaming ? _react2.default.createElement(
'audio',
{ className: 'http-streaming', autoPlay: true },
_react2.default.createElement('source', { src: this.props.http_streaming, type: 'audio/mpeg' })
) : null,
_react2.default.createElement( _react2.default.createElement(
'div', 'div',
{ className: 'current-track' }, { className: 'current-track' },
@ -58730,6 +58737,7 @@ var PlaybackControls = function (_React$Component) {
var mapStateToProps = function mapStateToProps(state, ownProps) { var mapStateToProps = function mapStateToProps(state, ownProps) {
return { return {
http_streaming: state.core.http_streaming_enabled && state.core.http_streaming_url ? state.core.http_streaming_url : '',
current_track: state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null, current_track: state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null,
radio_enabled: state.ui.radio && state.ui.radio.enabled ? true : false, radio_enabled: state.ui.radio && state.ui.radio.enabled ? true : false,
play_state: state.mopidy.play_state, play_state: state.mopidy.play_state,
@ -60367,7 +60375,7 @@ var _SearchURISchemesModal = __webpack_require__(410);
var _SearchURISchemesModal2 = _interopRequireDefault(_SearchURISchemesModal); var _SearchURISchemesModal2 = _interopRequireDefault(_SearchURISchemesModal);
var _InitialSetupModal = __webpack_require__(481); var _InitialSetupModal = __webpack_require__(414);
var _InitialSetupModal2 = _interopRequireDefault(_InitialSetupModal); var _InitialSetupModal2 = _interopRequireDefault(_InitialSetupModal);
@ -63827,7 +63835,139 @@ exports.default = Sortable;
/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(22))) /* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(22)))
/***/ }), /***/ }),
/* 414 */, /* 414 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = __webpack_require__(1);
var _react2 = _interopRequireDefault(_react);
var _reactFontawesome = __webpack_require__(6);
var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome);
var _Icon = __webpack_require__(21);
var _Icon2 = _interopRequireDefault(_Icon);
var _helpers = __webpack_require__(2);
var helpers = _interopRequireWildcard(_helpers);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var InitialSetupModal = function (_React$Component) {
_inherits(InitialSetupModal, _React$Component);
function InitialSetupModal(props) {
_classCallCheck(this, InitialSetupModal);
var _this = _possibleConstructorReturn(this, (InitialSetupModal.__proto__ || Object.getPrototypeOf(InitialSetupModal)).call(this, props));
_this.state = {
username: 'Anonymous'
};
return _this;
}
_createClass(InitialSetupModal, [{
key: 'handleSubmit',
value: function handleSubmit(e) {
// save and falseify show_initial_setup
}
}, {
key: 'handleCancel',
value: function handleCancel(e) {
// falseify show_initial_setup
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
return _react2.default.createElement(
'div',
null,
_react2.default.createElement(
'h1',
null,
'Welcome to Iris'
),
_react2.default.createElement(
'form',
{ onSubmit: function onSubmit(e) {
return _this2.handleSubmit(e);
} },
_react2.default.createElement(
'div',
{ className: 'field' },
_react2.default.createElement(
'div',
{ className: 'name' },
'Username'
),
_react2.default.createElement(
'div',
{ className: 'input' },
_react2.default.createElement('input', {
type: 'text',
onChange: function onChange(e) {
return _this2.setState({ username: e.target.value.replace(/\W/g, '') });
},
value: this.state.username }),
_react2.default.createElement(
'div',
{ className: 'description' },
'A non-unique string used to identify this client (no special characters)'
)
)
),
_react2.default.createElement(
'div',
{ className: 'actions centered-text' },
_react2.default.createElement(
'button',
{ className: 'large', onClick: function onClick(e) {
return _this2.handleCancel(e);
} },
'Skip'
),
_react2.default.createElement(
'button',
{ className: 'primary large', onClick: function onClick(e) {
return _this2.handleSubmit(e);
} },
'Save'
)
)
)
);
}
}]);
return InitialSetupModal;
}(_react2.default.Component);
exports.default = InitialSetupModal;
/***/ }),
/* 415 */ /* 415 */
/***/ (function(module, exports, __webpack_require__) { /***/ (function(module, exports, __webpack_require__) {
@ -67943,6 +68083,69 @@ var Settings = function (_React$Component) {
), ),
this.renderApplyButton() this.renderApplyButton()
), ),
_react2.default.createElement(
'h4',
{ className: 'underline' },
'Streaming'
),
_react2.default.createElement(
'div',
{ className: 'field checkbox' },
_react2.default.createElement(
'div',
{ className: 'name' },
'Enable'
),
_react2.default.createElement(
'div',
{ className: 'input' },
_react2.default.createElement(
'label',
null,
_react2.default.createElement('input', {
type: 'checkbox',
name: 'ssl',
checked: this.props.core.http_streaming_enabled,
onChange: function onChange(e) {
return _this3.props.coreActions.set({ http_streaming_enabled: !_this3.props.core.http_streaming_enabled });
} }),
_react2.default.createElement(
'span',
{ className: 'label has-tooltip' },
'Enable HTTP streaming',
_react2.default.createElement(
'span',
{ className: 'tooltip' },
'Requires streaming service like Icecast2'
)
)
)
)
),
_react2.default.createElement(
'div',
{ className: 'field' },
_react2.default.createElement(
'div',
{ className: 'name' },
'Stream location'
),
_react2.default.createElement(
'div',
{ className: 'input' },
_react2.default.createElement('input', {
type: 'text',
onChange: function onChange(e) {
return _this3.props.coreActions.set({ http_streaming_url: e.target.value });
},
value: this.props.core.http_streaming_url }),
_react2.default.createElement(
'div',
{ className: 'description' },
'The full URL to your stream endpoint'
)
)
),
_react2.default.createElement( _react2.default.createElement(
'h4', 'h4',
{ className: 'underline' }, { className: 'underline' },
@ -75305,158 +75508,6 @@ exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(
// removed by extract-text-webpack-plugin // removed by extract-text-webpack-plugin
/***/ }),
/* 462 */,
/* 463 */,
/* 464 */,
/* 465 */,
/* 466 */,
/* 467 */,
/* 468 */,
/* 469 */,
/* 470 */,
/* 471 */,
/* 472 */,
/* 473 */,
/* 474 */,
/* 475 */,
/* 476 */,
/* 477 */,
/* 478 */,
/* 479 */,
/* 480 */,
/* 481 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _react = __webpack_require__(1);
var _react2 = _interopRequireDefault(_react);
var _reactFontawesome = __webpack_require__(6);
var _reactFontawesome2 = _interopRequireDefault(_reactFontawesome);
var _Icon = __webpack_require__(21);
var _Icon2 = _interopRequireDefault(_Icon);
var _helpers = __webpack_require__(2);
var helpers = _interopRequireWildcard(_helpers);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var InitialSetupModal = function (_React$Component) {
_inherits(InitialSetupModal, _React$Component);
function InitialSetupModal(props) {
_classCallCheck(this, InitialSetupModal);
var _this = _possibleConstructorReturn(this, (InitialSetupModal.__proto__ || Object.getPrototypeOf(InitialSetupModal)).call(this, props));
_this.state = {
username: 'Anonymous'
};
return _this;
}
_createClass(InitialSetupModal, [{
key: 'handleSubmit',
value: function handleSubmit(e) {
// save and falseify show_initial_setup
}
}, {
key: 'handleCancel',
value: function handleCancel(e) {
// falseify show_initial_setup
}
}, {
key: 'render',
value: function render() {
var _this2 = this;
return _react2.default.createElement(
'div',
null,
_react2.default.createElement(
'h1',
null,
'Welcome to Iris'
),
_react2.default.createElement(
'form',
{ onSubmit: function onSubmit(e) {
return _this2.handleSubmit(e);
} },
_react2.default.createElement(
'div',
{ className: 'field' },
_react2.default.createElement(
'div',
{ className: 'name' },
'Username'
),
_react2.default.createElement(
'div',
{ className: 'input' },
_react2.default.createElement('input', {
type: 'text',
onChange: function onChange(e) {
return _this2.setState({ username: e.target.value.replace(/\W/g, '') });
},
value: this.state.username }),
_react2.default.createElement(
'div',
{ className: 'description' },
'A non-unique string used to identify this client (no special characters)'
)
)
),
_react2.default.createElement(
'div',
{ className: 'actions centered-text' },
_react2.default.createElement(
'button',
{ className: 'large', onClick: function onClick(e) {
return _this2.handleCancel(e);
} },
'Skip'
),
_react2.default.createElement(
'button',
{ className: 'primary large', onClick: function onClick(e) {
return _this2.handleSubmit(e);
} },
'Save'
)
)
)
);
}
}]);
return InitialSetupModal;
}(_react2.default.Component);
exports.default = InitialSetupModal;
/***/ }) /***/ })
/******/ ]); /******/ ]);
//# sourceMappingURL=app.js.map //# sourceMappingURL=app.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,84 @@
@charset "UTF-8"; .input-range__slider {
appearance: none;
background: #3f51b5;
border: 1px solid #3f51b5;
border-radius: 100%;
cursor: pointer;
display: block;
height: 1rem;
margin-left: -0.5rem;
margin-top: -0.65rem;
outline: none;
position: absolute;
top: 50%;
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;
width: 1rem; }
.input-range__slider:active {
transform: scale(1.3); }
.input-range__slider:focus {
box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2); }
.input-range--disabled .input-range__slider {
background: #cccccc;
border: 1px solid #cccccc;
box-shadow: none;
transform: none; }
.input-range__slider-container {
transition: left 0.3s ease-out; }
.input-range__label {
color: #aaaaaa;
font-family: "Helvetica Neue", san-serif;
font-size: 0.8rem;
transform: translateZ(0);
white-space: nowrap; }
.input-range__label--min,
.input-range__label--max {
bottom: -1.4rem;
position: absolute; }
.input-range__label--min {
left: 0; }
.input-range__label--max {
right: 0; }
.input-range__label--value {
position: absolute;
top: -1.8rem; }
.input-range__label-container {
left: -50%;
position: relative; }
.input-range__label--max .input-range__label-container {
left: 50%; }
.input-range__track {
background: #eeeeee;
border-radius: 0.3rem;
cursor: pointer;
display: block;
height: 0.3rem;
position: relative;
transition: left 0.3s ease-out, width 0.3s ease-out; }
.input-range--disabled .input-range__track {
background: #eeeeee; }
.input-range__track--background {
left: 0;
margin-top: -0.15rem;
position: absolute;
right: 0;
top: 50%; }
.input-range__track--active {
background: #3f51b5; }
.input-range {
height: 1rem;
position: relative;
width: 100%; }@charset "UTF-8";
/*! /*!
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
@ -2360,13 +2440,16 @@
@keyframes slideloader { @keyframes slideloader {
0% { 0% {
left: 0%; left: 0%;
right: 100%; } right: 100%;
border-radius: 3px 0 0 3px; }
50% { 50% {
left: 0%; left: 0%;
right: 0%; } right: 0%;
border-radius: 3px; }
100% { 100% {
left: 100%; left: 100%;
right: 0%; } } right: 0%;
border-radius: 0 3px 3px 0; } }
/** /**
* Grid layout * Grid layout
@ -2763,7 +2846,7 @@ footer {
.flag.warning { .flag.warning {
background: #f16f19; background: #f16f19;
color: #FFFFFF; } color: #FFFFFF; }
.flag.bad { .flag.error, .flag.bad {
background: #cf2d2d; background: #cf2d2d;
color: #FFFFFF; } color: #FFFFFF; }
.flag.dark { .flag.dark {
@ -2777,6 +2860,25 @@ footer {
line-height: 1.4em; line-height: 1.4em;
vertical-align: middle; } vertical-align: middle; }
.message {
padding: 10px 12px 8px;
background: #FFF39C;
color: #181818;
clear: both;
font-size: 14px;
border-radius: 3px; }
.message.info {
background: #FFF39C; }
.message.notice {
background: #32b5f2;
color: #FFFFFF; }
.message.warning {
background: #f16f19;
color: #FFFFFF; }
.message.error, .message.bad {
background: #cf2d2d;
color: #FFFFFF; }
@media (max-width: 800px) { @media (max-width: 800px) {
h1 { h1 {
font-size: 1.8rem; font-size: 1.8rem;
@ -2935,6 +3037,22 @@ input[type="submit"] {
background: #f2f2f2; background: #f2f2f2;
margin-top: 1px; margin-top: 1px;
margin-bottom: 9px; } margin-bottom: 9px; }
.button.small,
button.small,
input[type="button"].small,
input[type="submit"].small {
padding: 6px 18px 4px; }
.button.large,
button.large,
input[type="button"].large,
input[type="submit"].large {
padding: 16px 50px 14px; }
.button.pull-right,
button.pull-right,
input[type="button"].pull-right,
input[type="submit"].pull-right {
margin-right: 0;
margin-left: 30px; }
.button.primary, .button.primary,
button.primary, button.primary,
input[type="button"].primary, input[type="button"].primary,
@ -3011,7 +3129,9 @@ input[type="submit"] {
input[type="button"].working, input[type="button"].working,
input[type="submit"].working { input[type="submit"].working {
position: relative; position: relative;
cursor: wait !important; } cursor: wait !important;
background: #383734;
color: #a3a19f; }
.button.working:after, .button.working:after,
button.working:after, button.working:after,
input[type="button"].working:after, input[type="button"].working:after,
@ -3021,7 +3141,7 @@ input[type="submit"] {
display: block; display: block;
content: ''; content: '';
animation: slideloader 1s infinite; animation: slideloader 1s infinite;
background: rgba(200, 200, 200, 0.4); background: rgba(200, 200, 200, 0.3);
top: 0; top: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
@ -3094,7 +3214,7 @@ input[type="submit"] {
display: inline-block; display: inline-block;
float: none; float: none;
width: auto; width: auto;
padding: 5px 30px 5px 30px; padding: 5px 5px 5px 26px;
position: relative; position: relative;
cursor: pointer; } cursor: pointer; }
.field.radio input + .label:before { .field.radio input + .label:before {
@ -3125,6 +3245,37 @@ input[type="submit"] {
left: 0; } left: 0; }
.field.checkbox input:checked + .label:before { .field.checkbox input:checked + .label:before {
content: '\F00C'; } content: '\F00C'; }
.field.range {
position: relative; }
.field.range .input-range {
height: auto; }
.field.range .label {
padding-bottom: 6px; }
.field.range .input-range__track,
.field.range .input-range__slider-container {
transition: none;
height: 6px;
border-radius: 3px;
cursor: col-resize; }
.field.range .input-range__track--background {
margin: 0;
background: rgba(100, 100, 100, 0.5); }
.field.range .input-range__track--active {
background: #32b5f2; }
.field.range .input-range__slider-container .input-range__slider {
cursor: col-resize;
background: #32b5f2;
border: none;
top: 0;
height: 6px;
width: 20px;
margin: -6px 0 0 0;
border-radius: 3px 0 0 3px; }
.field.range .input-range__slider-container:last-child .input-range__slider {
border-radius: 0 3px 3px 0;
margin-left: -12px; }
.field.sub-field {
padding: 0; }
.field .description { .field .description {
display: inline-block; display: inline-block;
padding: 10px 14px; padding: 10px 14px;
@ -3413,6 +3564,9 @@ input[type="submit"] {
background: -moz-linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black); background: -moz-linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black);
background: -o-linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black); background: -o-linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black);
background: linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black); } background: linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black); }
h4 .context-menu-trigger,
h4 .context-menu-trigger:active {
margin: -8px 0 0 10px; }
.context-menu-trigger .dot { .context-menu-trigger .dot {
width: 6px; width: 6px;
height: 6px; height: 6px;
@ -4986,19 +5140,21 @@ main header {
font-family: "Overpass"; font-family: "Overpass";
position: relative; position: relative;
display: inline-block; } display: inline-block; }
.dropdown-field .label { .dropdown-field .label:not(.button) {
-webkit-transition: all 0.2s ease-in-out; -webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out; -moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out; -o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
padding: 14px 10px; padding: 14px 10px;
cursor: pointer; } cursor: pointer; }
.notouch .dropdown-field .label:hover { .notouch .dropdown-field .label:not(.button):hover {
color: #32b5f2; } color: #32b5f2; }
.dropdown-field.expanded .label { .dropdown-field.expanded .label:not(.button) {
color: #32b5f2; } color: #32b5f2; }
.dropdown-field.expanded .options { .dropdown-field.expanded .options {
display: block; } display: block; }
.dropdown-field.no-status-icon .options .option {
padding-left: 12px; }
.dropdown-field .options { .dropdown-field .options {
color: #000000; color: #000000;
position: absolute; position: absolute;
@ -5226,6 +5382,8 @@ main header {
.has-tooltip:hover .tooltip { .has-tooltip:hover .tooltip {
opacity: 1; opacity: 1;
bottom: 35px; } bottom: 35px; }
.has-tooltip.info .fa {
opacity: 0.3; }
.has-tooltip.large-tooltip .tooltip { .has-tooltip.large-tooltip .tooltip {
padding: 8px 12px; padding: 8px 12px;
font-size: 12px; font-size: 12px;
@ -5690,53 +5848,106 @@ main .track-view {
color: #FFFFFF; } color: #FFFFFF; }
.discover-view .intro .liner h2 { .discover-view .intro .liner h2 {
padding-bottom: 8px; } padding-bottom: 8px; }
.discover-view .intro .liner h3 {
padding-bottom: 40px;
opacity: 0.5; }
.discover-view .seeds { .discover-view .parameters {
padding-top: 40px; } padding-top: 50px; }
.discover-view .seeds .seed { .discover-view .parameters::after {
display: inline-block; content: '';
background: #32b5f2; clear: both;
color: #FFFFFF; display: block; }
padding: 8px 26px 8px 14px; .discover-view .parameters .seeds,
.discover-view .parameters .tunabilities {
float: left; }
.discover-view .seeds .seed {
color: #FFFFFF;
padding: 0 30px 30px 0;
float: left; }
.discover-view .seeds .seed .thumbnail-wrapper {
float: left;
width: 44px;
border: 0 !important; }
.discover-view .seeds .seed .label .remove {
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
cursor: pointer;
font-size: 10px;
padding: 4px;
margin-left: 4px; }
.discover-view .seeds .seed .label .remove:hover {
color: #cf2d2d; }
.discover-view .seeds .seed .name {
font-size: 16px; font-size: 16px;
position: relative; padding-top: 3px; }
vertical-align: top; .discover-view .seeds .seed.has-thumbnail .label,
margin: 0 10px 10px 0; .discover-view .seeds .seed.has-thumbnail .name {
border-radius: 3px; } padding-left: 52px; }
.discover-view .seeds .seed .type {
display: inline-block;
font-size: 12px;
opacity: 0.5;
padding: 0 10px 0 5px; }
.discover-view .seeds .seed .remove {
font-size: 10px;
padding: 12px;
position: absolute;
top: 0;
right: 0;
cursor: pointer; }
.discover-view .autocomplete-field { .discover-view .tunabilities::after {
width: 250px; content: '';
display: inline-block; clear: both;
vertical-align: top; display: block; }
padding: 0; }
.discover-view .autocomplete-field .input { .discover-view .tunabilities .tunability {
width: 200px;
float: left;
box-sizing: border-box;
padding: 0 30px 30px 0;
position: relative; }
.discover-view .tunabilities .tunability .input {
float: none; float: none;
margin: 0 10px 10px 0; } width: 100%;
.discover-view .autocomplete-field .input input { padding-top: 4px; }
background: rgba(100, 100, 100, 0.5); .discover-view .tunabilities .tunability .input-range .input-range__label-container {
color: #FFFFFF; display: none; }
padding: 7px 13px; .discover-view .tunabilities .tunability .remove {
font-size: 16px; -webkit-transition: all 0.2s ease-in-out;
border: 1px solid transparent; } -moz-transition: all 0.2s ease-in-out;
.discover-view .autocomplete-field .input input:focus, .discover-view .autocomplete-field .input input:active { -o-transition: all 0.2s ease-in-out;
border-color: #32b5f2; } transition: all 0.2s ease-in-out;
.discover-view .autocomplete-field .input:after { cursor: pointer;
background: #32b5f2; } font-size: 10px;
padding: 4px;
margin-left: 4px; }
.discover-view .tunabilities .tunability .remove:hover {
color: #cf2d2d; }
.discover-view .actions {
padding-top: 40px; }
.discover-view .actions .autocomplete-field {
width: 250px;
display: inline-block;
vertical-align: top;
padding: 0;
margin-right: 1px; }
.discover-view .actions .autocomplete-field .input {
float: none;
margin: 0 10px 10px 0; }
.discover-view .actions .autocomplete-field .input input {
background: rgba(100, 100, 100, 0.5);
color: #FFFFFF;
padding: 15px 13px 10px;
font-size: 16px;
border: 1px solid transparent;
border-top-right-radius: 0;
border-bottom-right-radius: 0; }
.discover-view .actions .autocomplete-field .input input:focus, .discover-view .actions .autocomplete-field .input input:active {
border-color: #32b5f2; }
.discover-view .actions .autocomplete-field .input:after {
background: #32b5f2; }
.discover-view .actions .autocomplete-field .results {
top: 49px; }
.discover-view .actions .dropdown-field .button {
background: rgba(100, 100, 100, 0.5);
color: #FFFFFF;
padding: 16px 20px 14px 20px;
border-top-left-radius: 0;
border-bottom-left-radius: 0; }
.discover-view .actions .dropdown-field .options {
top: 50px;
right: 20px; }
.discover-view section .grid { .discover-view section .grid {
padding-bottom: 20px; } padding-bottom: 20px; }
@ -5744,8 +5955,22 @@ main .track-view {
@media (max-width: 800px) { @media (max-width: 800px) {
.discover-view .intro { .discover-view .intro {
padding: 40px 20px; } padding: 40px 20px; }
.discover-view .seeds { .discover-view .parameters .seeds {
padding-top: 20px; } } padding-top: 20px;
float: none; }
.discover-view .parameters .seeds .seed {
float: none; }
.discover-view .parameters .tunabilities {
float: none; }
.discover-view .parameters .tunabilities .tunability {
width: 100%;
float: none; }
.discover-view .parameters .add .add-properties {
margin-right: 0; }
.discover-view .col.tracks {
width: 100%; }
.discover-view .col.others, .discover-view .col.w5 {
display: none; } }
.discover-featured-view .intro, .discover-featured-view .intro,
.discover-new-releases-view .intro { .discover-new-releases-view .intro {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -38,7 +38,7 @@
// Release details // Release details
// These are automatically injected to built HTML // These are automatically injected to built HTML
var build = "1517341290"; var build = "1517428265";
var version = "3.11.0"; var version = "3.11.0";
// Construct the script tag // Construct the script tag

4
src/js/bootstrap.js vendored
View File

@ -41,7 +41,9 @@ var initialState = {
artists: {}, artists: {},
playlists: {}, playlists: {},
users: {}, users: {},
tracks: {} tracks: {},
http_streaming_enabled: false,
http_streaming_url: "http://"+window.location.hostname+":8000/mopidy"
}, },
ui: { ui: {
show_initial_setup: true, show_initial_setup: true,

View File

@ -70,6 +70,10 @@ class PlaybackControls extends React.Component{
return ( return (
<div className={(this.state.expanded ? "expanded playback-controls" : "playback-controls")}> <div className={(this.state.expanded ? "expanded playback-controls" : "playback-controls")}>
{this.props.http_streaming ? <audio className="http-streaming" autoPlay>
<source src={this.props.http_streaming} type="audio/mpeg" />
</audio> : null}
<div className="current-track"> <div className="current-track">
<div className="thumbnail-wrapper" onClick={e => this.handleThumbnailClick(e)}> <div className="thumbnail-wrapper" onClick={e => this.handleThumbnailClick(e)}>
@ -135,6 +139,7 @@ class PlaybackControls extends React.Component{
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
return { return {
http_streaming: (state.core.http_streaming_enabled && state.core.http_streaming_url ? state.core.http_streaming_url : ''),
current_track: (state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null), current_track: (state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null),
radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false), radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false),
play_state: state.mopidy.play_state, play_state: state.mopidy.play_state,

View File

@ -331,7 +331,38 @@ class Settings extends React.Component {
</div> </div>
</div> </div>
{this.renderApplyButton()} {this.renderApplyButton()}
</form> </form>
<h4 className="underline">Streaming</h4>
<div className="field checkbox">
<div className="name">Enable</div>
<div className="input">
<label>
<input
type="checkbox"
name="ssl"
checked={this.props.core.http_streaming_enabled}
onChange={e => this.props.coreActions.set({http_streaming_enabled: !this.props.core.http_streaming_enabled})} />
<span className="label has-tooltip">
Enable HTTP streaming
<span className="tooltip">Requires streaming service like Icecast2</span>
</span>
</label>
</div>
</div>
<div className="field">
<div className="name">Stream location</div>
<div className="input">
<input
type="text"
onChange={e => this.props.coreActions.set({http_streaming_url: e.target.value})}
value={this.props.core.http_streaming_url} />
<div className="description">
The full URL to your stream endpoint
</div>
</div>
</div>
<h4 className="underline">Localization</h4> <h4 className="underline">Localization</h4>