diff --git a/aircox/models/station.py b/aircox/models/station.py
index a87a2db..aaf6ae3 100644
--- a/aircox/models/station.py
+++ b/aircox/models/station.py
@@ -2,6 +2,7 @@ import os
from django.db import models
from django.utils.translation import gettext_lazy as _
+from django.utils.functional import cached_property
from filer.fields.image import FilerImageField
@@ -74,6 +75,11 @@ class Station(models.Model):
objects = StationQuerySet.as_manager()
+ @cached_property
+ def streams(self):
+ """ Audio streams as list of urls. """
+ return self.audio_streams.split('\n') if self.audio_streams else []
+
def __str__(self):
return self.name
diff --git a/aircox/static/aircox/css/chunk-common.css b/aircox/static/aircox/css/chunk-common.css
index 264f520..90019a9 100644
--- a/aircox/static/aircox/css/chunk-common.css
+++ b/aircox/static/aircox/css/chunk-common.css
@@ -10807,6 +10807,9 @@ a.navbar-item.is-active {
.player .player-bar {
border-top: 1px hsl(0deg, 0%, 71%) solid;
}
+.player .player-bar > div {
+ height: 3.75em !important;
+}
.player .player-bar > .media-left:not(:last-child) {
margin-right: 0em;
}
@@ -10823,7 +10826,8 @@ a.navbar-item.is-active {
}
.player .player-bar .button {
font-size: 1.5rem !important;
- height: 2.5em;
+ height: 100%;
+ padding: auto 0.2em !important;
min-width: 2.5em;
border-radius: 0px;
transition: background-color 1s;
diff --git a/aircox/static/aircox/js/chunk-common.js b/aircox/static/aircox/js/chunk-common.js
index 4985c9e..74983bd 100644
--- a/aircox/static/aircox/js/chunk-common.js
+++ b/aircox/static/aircox/js/chunk-common.js
@@ -65,7 +65,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */
\*****************************************************************************************************************************************************************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"State\": function() { return /* binding */ State; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n/* harmony import */ var _live__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../live */ \"./src/live.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../model */ \"./src/model.js\");\n/* harmony import */ var _APlaylist__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./APlaylist */ \"./src/components/APlaylist.vue\");\n/* harmony import */ var _AProgress__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./AProgress */ \"./src/components/AProgress.vue\");\n\n\n\n\n\n\n\nconst State = {\n paused: 0,\n playing: 1,\n loading: 2\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n components: {\n APlaylist: _APlaylist__WEBPACK_IMPORTED_MODULE_5__[\"default\"],\n AProgress: _AProgress__WEBPACK_IMPORTED_MODULE_6__[\"default\"]\n },\n data() {\n let audio = new Audio();\n audio.addEventListener('ended', e => this.onState(e));\n audio.addEventListener('pause', e => this.onState(e));\n audio.addEventListener('playing', e => this.onState(e));\n audio.addEventListener('timeupdate', () => {\n this.currentTime = this.audio.currentTime;\n });\n audio.addEventListener('durationchange', () => {\n this.duration = Number.isFinite(this.audio.duration) ? this.audio.duration : null;\n });\n let live = this.liveArgs ? (0,vue__WEBPACK_IMPORTED_MODULE_1__.reactive)(new _live__WEBPACK_IMPORTED_MODULE_2__[\"default\"](this.liveArgs)) : null;\n live && live.refresh();\n return {\n audio,\n duration: 0,\n currentTime: 0,\n state: State.paused,\n live,\n /// Loaded item\n loaded: null,\n //! Active panel name\n panel: null,\n //! current playing playlist name\n playlistName: null,\n //! players' playlists' sets\n sets: {\n queue: _model__WEBPACK_IMPORTED_MODULE_4__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"], \"playlist.queue\", {\n max: 30,\n unique: true\n }),\n pin: _model__WEBPACK_IMPORTED_MODULE_4__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"], \"player.pin\", {\n max: 30,\n unique: true\n })\n }\n };\n },\n props: {\n buttonTitle: String,\n liveArgs: Object\n },\n computed: {\n self() {\n return this;\n },\n paused() {\n return this.state == State.paused;\n },\n playing() {\n return this.state == State.playing;\n },\n loading() {\n return this.state == State.loading;\n },\n playlist() {\n return this.playlistName ? this.$refs[this.playlistName] : null;\n },\n current() {\n return this.loaded ? this.loaded : this.live && this.live.current;\n }\n },\n methods: {\n displayTime(seconds) {\n seconds = parseInt(seconds);\n let s = seconds % 60;\n seconds = (seconds - s) / 60;\n let m = seconds % 60;\n let h = (seconds - m) / 60;\n let [ss, mm, hh] = [s.toString().padStart(2, '0'), m.toString().padStart(2, '0'), h.toString().padStart(2, '0')];\n return h ? `${hh}:${mm}:${ss}` : `${mm}:${ss}`;\n },\n playlistButtonClass(name) {\n let set = this.sets[name];\n return (set ? (set.length ? \"\" : \"has-text-grey-light \") + (this.panel == name ? \"is-info \" : this.playlistName == name ? 'is-primary ' : '') : '') + \"button has-text-weight-bold\";\n },\n /// Show/hide panel\n togglePanel(panel) {\n this.panel = this.panel == panel ? null : panel;\n },\n /// Return True if item is loaded\n isLoaded(item) {\n return this.loaded && this.loaded.id == item.id;\n },\n /// Return True if item is loaded\n isPlaying(item) {\n return this.isLoaded(item) && !this.paused;\n },\n _setPlaylist(playlist) {\n this.playlistName = playlist;\n for (var p in this.sets) if (p != playlist) this.$refs[p].unselect();\n },\n /// Load a sound from playlist or live\n load(playlist = null, index = 0) {\n let src = null;\n\n // from playlist\n if (playlist !== null && index != -1) {\n let item = this.$refs[playlist].get(index);\n if (!item) throw `No sound at index ${index} for playlist ${playlist}`;\n this.loaded = item;\n this.current = item;\n src = item.src;\n }\n // from live\n else {\n this.loaded = null;\n this.current = this.live.current;\n src = this.live.src;\n }\n this._setPlaylist(playlist);\n\n // load sources\n const audio = this.audio;\n if (src instanceof Array) {\n audio.innerHTML = '';\n audio.removeAttribute('src');\n for (var s of src) {\n let source = document.createElement('source');\n source.setAttribute('src', s);\n audio.appendChild(source);\n }\n } else {\n audio.src = src;\n }\n audio.load();\n },\n play(playlist = null, index = 0) {\n this.load(playlist, index);\n this.audio.play().catch(e => console.error(e));\n },\n /// Push items to playlist (by name)\n push(playlist, ...items) {\n return this.sets[playlist].push(...items);\n },\n /// Push and play items\n playItems(playlist, ...items) {\n let index = this.push(playlist, ...items);\n this.$refs[playlist].selectedIndex = index;\n this.play(playlist, index);\n },\n /// Handle click event that plays multiple items (from `data-sounds` attribute)\n playButtonClick(event) {\n var items = JSON.parse(event.currentTarget.dataset.sounds);\n this.playItems('queue', ...items);\n },\n /// Pause\n pause() {\n this.audio.pause();\n },\n //! Play/pause\n togglePlay(playlist = null, index = 0) {\n if (playlist !== null) {\n let item = this.sets[playlist].get(index);\n if (!this.playlist || this.playlistName !== playlist || this.loaded != item) {\n this.play(playlist, index);\n return;\n }\n }\n if (this.paused) this.audio.play().catch(e => console.error(e));else this.audio.pause();\n },\n //! Pin/Unpin an item\n togglePin(item) {\n let index = this.sets.pin.findIndex(item);\n if (index > -1) this.sets.pin.remove(index);else {\n this.sets.pin.push(item);\n this.$refs.pinPlaylistButton.focus();\n }\n },\n /// Audio player state change event\n onState(event) {\n const audio = this.audio;\n this.state = audio.paused ? State.paused : State.playing;\n if (event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1)) this.play();\n }\n },\n mounted() {\n this.load();\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlayer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"State\": function() { return /* binding */ State; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n/* harmony import */ var _live__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../live */ \"./src/live.js\");\n/* harmony import */ var _sound__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../sound */ \"./src/sound.js\");\n/* harmony import */ var _model__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../model */ \"./src/model.js\");\n/* harmony import */ var _APlaylist__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./APlaylist */ \"./src/components/APlaylist.vue\");\n/* harmony import */ var _AProgress__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./AProgress */ \"./src/components/AProgress.vue\");\n\n\n\n\n\n\n\nconst State = {\n paused: 0,\n playing: 1,\n loading: 2\n};\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n components: {\n APlaylist: _APlaylist__WEBPACK_IMPORTED_MODULE_5__[\"default\"],\n AProgress: _AProgress__WEBPACK_IMPORTED_MODULE_6__[\"default\"]\n },\n data() {\n let audio = new Audio();\n audio.addEventListener('ended', e => this.onState(e));\n audio.addEventListener('pause', e => this.onState(e));\n audio.addEventListener('playing', e => this.onState(e));\n audio.addEventListener('timeupdate', () => {\n this.currentTime = this.audio.currentTime;\n });\n audio.addEventListener('durationchange', () => {\n this.duration = Number.isFinite(this.audio.duration) ? this.audio.duration : null;\n });\n let live = this.liveArgs ? (0,vue__WEBPACK_IMPORTED_MODULE_1__.reactive)(new _live__WEBPACK_IMPORTED_MODULE_2__[\"default\"](this.liveArgs)) : null;\n live && live.refresh();\n return {\n audio,\n duration: 0,\n currentTime: 0,\n state: State.paused,\n live,\n /// Loaded item\n loaded: null,\n //! Active panel name\n panel: null,\n //! current playing playlist name\n playlistName: null,\n //! players' playlists' sets\n sets: {\n queue: _model__WEBPACK_IMPORTED_MODULE_4__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"], \"playlist.queue\", {\n max: 30,\n unique: true\n }),\n pin: _model__WEBPACK_IMPORTED_MODULE_4__.Set.storeLoad(_sound__WEBPACK_IMPORTED_MODULE_3__[\"default\"], \"player.pin\", {\n max: 30,\n unique: true\n })\n }\n };\n },\n props: {\n buttonTitle: String,\n liveArgs: Object\n },\n computed: {\n self() {\n return this;\n },\n paused() {\n return this.state == State.paused;\n },\n playing() {\n return this.state == State.playing;\n },\n loading() {\n return this.state == State.loading;\n },\n playlist() {\n return this.playlistName ? this.$refs[this.playlistName] : null;\n },\n current() {\n return this.loaded ? this.loaded : this.live && this.live.current;\n }\n },\n methods: {\n displayTime(seconds) {\n seconds = parseInt(seconds);\n let s = seconds % 60;\n seconds = (seconds - s) / 60;\n let m = seconds % 60;\n let h = (seconds - m) / 60;\n let [ss, mm, hh] = [s.toString().padStart(2, '0'), m.toString().padStart(2, '0'), h.toString().padStart(2, '0')];\n return h ? `${hh}:${mm}:${ss}` : `${mm}:${ss}`;\n },\n playlistButtonClass(name) {\n let set = this.sets[name];\n return (set ? (set.length ? \"\" : \"has-text-grey-light \") + (this.panel == name ? \"is-info \" : this.playlistName == name ? 'is-primary ' : '') : '') + \"button has-text-weight-bold\";\n },\n /// Show/hide panel\n togglePanel(panel) {\n this.panel = this.panel == panel ? null : panel;\n },\n /// Return True if item is loaded\n isLoaded(item) {\n return this.loaded && this.loaded.id == item.id;\n },\n /// Return True if item is loaded\n isPlaying(item) {\n return this.isLoaded(item) && !this.paused;\n },\n _setPlaylist(playlist) {\n this.playlistName = playlist;\n for (var p in this.sets) if (p != playlist) this.$refs[p].unselect();\n },\n /// Load a sound from playlist or live\n load(playlist = null, index = 0) {\n let src = null;\n\n // from playlist\n if (playlist !== null && index != -1) {\n let item = this.$refs[playlist].get(index);\n if (!item) throw `No sound at index ${index} for playlist ${playlist}`;\n this.loaded = item;\n src = item.src;\n }\n // from live\n else {\n this.loaded = null;\n src = this.live.src;\n }\n this._setPlaylist(playlist);\n\n // load sources\n const audio = this.audio;\n if (src instanceof Array) {\n audio.innerHTML = '';\n audio.removeAttribute('src');\n for (var s of src) {\n let source = document.createElement('source');\n source.setAttribute('src', s);\n audio.appendChild(source);\n }\n } else {\n audio.src = src;\n }\n audio.load();\n },\n play(playlist = null, index = 0) {\n this.load(playlist, index);\n this.audio.play().catch(e => console.error(e));\n },\n /// Push items to playlist (by name)\n push(playlist, ...items) {\n return this.sets[playlist].push(...items);\n },\n /// Push and play items\n playItems(playlist, ...items) {\n let index = this.push(playlist, ...items);\n this.$refs[playlist].selectedIndex = index;\n this.play(playlist, index);\n },\n /// Handle click event that plays multiple items (from `data-sounds` attribute)\n playButtonClick(event) {\n var items = JSON.parse(event.currentTarget.dataset.sounds);\n this.playItems('queue', ...items);\n },\n /// Pause\n pause() {\n this.audio.pause();\n },\n //! Play/pause\n togglePlay(playlist = null, index = 0) {\n if (playlist !== null) {\n let item = this.sets[playlist].get(index);\n if (!this.playlist || this.playlistName !== playlist || this.loaded != item) {\n this.play(playlist, index);\n return;\n }\n }\n if (this.paused) this.audio.play().catch(e => console.error(e));else this.audio.pause();\n },\n //! Pin/Unpin an item\n togglePin(item) {\n let index = this.sets.pin.findIndex(item);\n if (index > -1) this.sets.pin.remove(index);else {\n this.sets.pin.push(item);\n this.$refs.pinPlaylistButton.focus();\n }\n },\n /// Audio player state change event\n onState(event) {\n const audio = this.audio;\n this.state = audio.paused ? State.paused : State.playing;\n if (event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1)) this.play();\n }\n },\n mounted() {\n this.load();\n }\n});\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlayer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D");
/***/ }),
@@ -205,7 +205,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac
\*********************************************************************************************************************************************************************************************************************************************************************/
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
-eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"render\": function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"player\"\n};\nconst _hoisted_2 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", {\n class: \"menu-label\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-thumbtack\"\n})]), /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)(\" Pinned \")], -1 /* HOISTED */);\nconst _hoisted_3 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", {\n class: \"menu-label\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-list\"\n})]), /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)(\" Playlist \")], -1 /* HOISTED */);\nconst _hoisted_4 = {\n class: \"player-bar media\"\n};\nconst _hoisted_5 = {\n class: \"media-left\"\n};\nconst _hoisted_6 = [\"title\", \"aria-label\"];\nconst _hoisted_7 = {\n key: 0,\n class: \"fas fa-pause\"\n};\nconst _hoisted_8 = {\n key: 1,\n class: \"fas fa-play\"\n};\nconst _hoisted_9 = {\n key: 0,\n class: \"media-left media-cover\"\n};\nconst _hoisted_10 = [\"src\"];\nconst _hoisted_11 = {\n class: \"media-content\"\n};\nconst _hoisted_12 = {\n class: \"media-right\"\n};\nconst _hoisted_13 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon is-size-6 has-text-danger\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-circle\"\n})], -1 /* HOISTED */);\nconst _hoisted_14 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"Live\", -1 /* HOISTED */);\nconst _hoisted_15 = [_hoisted_13, _hoisted_14];\nconst _hoisted_16 = {\n key: 0,\n class: \"mr-2 is-size-6\"\n};\nconst _hoisted_17 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-thumbtack\"\n})], -1 /* HOISTED */);\nconst _hoisted_18 = {\n key: 0,\n class: \"mr-2 is-size-6\"\n};\nconst _hoisted_19 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-list\"\n})], -1 /* HOISTED */);\n\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n const _component_APlaylist = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"APlaylist\");\n const _component_AProgress = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"AProgress\");\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(['player-panels', $data.panel ? 'is-open' : ''])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_APlaylist, {\n ref: \"pin\",\n class: \"player-panel menu\",\n name: \"Pinned\",\n actions: ['page'],\n editable: true,\n player: $options.self,\n set: $data.sets.pin,\n onSelect: _cache[0] || (_cache[0] = $event => $options.togglePlay('pin', $event.index)),\n listClass: \"menu-list\",\n itemClass: \"menu-item\"\n }, {\n header: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withCtx)(() => [_hoisted_2]),\n _: 1 /* STABLE */\n }, 8 /* PROPS */, [\"player\", \"set\"]), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, $data.panel == 'pin']]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_APlaylist, {\n ref: \"queue\",\n class: \"player-panel menu\",\n actions: ['page'],\n editable: true,\n player: $options.self,\n set: $data.sets.queue,\n onSelect: _cache[1] || (_cache[1] = $event => $options.togglePlay('queue', $event.index)),\n listClass: \"menu-list\",\n itemClass: \"menu-item\"\n }, {\n header: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withCtx)(() => [_hoisted_3]),\n _: 1 /* STABLE */\n }, 8 /* PROPS */, [\"player\", \"set\"]), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, $data.panel == 'queue']])], 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_4, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_5, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"button\",\n onClick: _cache[2] || (_cache[2] = $event => $options.togglePlay()),\n title: $props.buttonTitle,\n \"aria-label\": $props.buttonTitle\n }, [$options.playing ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_7)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_8))], 8 /* PROPS */, _hoisted_6)]), $options.current && $options.current.data.cover ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"img\", {\n src: $options.current.data.cover,\n class: \"cover\"\n }, null, 8 /* PROPS */, _hoisted_10)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_11, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.renderSlot)(_ctx.$slots, \"content\", {\n loaded: $data.loaded,\n live: $data.live,\n current: $options.current\n }), $data.loaded && $data.duration ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_AProgress, {\n key: 0,\n value: $data.currentTime,\n max: this.duration,\n format: $options.displayTime,\n class: \"pt-1 is-size-7\",\n onSelect: _cache[3] || (_cache[3] = $event => $data.audio.currentTime = $event)\n }, null, 8 /* PROPS */, [\"value\", \"max\", \"format\"])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_12, [$data.loaded ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 0,\n class: \"button has-text-weight-bold\",\n onClick: _cache[4] || (_cache[4] = $event => $options.play())\n }, _hoisted_15)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n ref: \"pinPlaylistButton\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)($options.playlistButtonClass('pin')),\n onClick: _cache[5] || (_cache[5] = $event => $options.togglePanel('pin'))\n }, [$data.sets.pin.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($data.sets.pin.length), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), _hoisted_17], 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)($options.playlistButtonClass('queue')),\n onClick: _cache[6] || (_cache[6] = $event => $options.togglePanel('queue'))\n }, [$data.sets.queue.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_18, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($data.sets.queue.length), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), _hoisted_19], 2 /* CLASS */)])])]);\n}\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlayer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D");
+eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"render\": function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"player\"\n};\nconst _hoisted_2 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", {\n class: \"menu-label\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-thumbtack\"\n})]), /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)(\" Pinned \")], -1 /* HOISTED */);\nconst _hoisted_3 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", {\n class: \"menu-label\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-list\"\n})]), /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)(\" Playlist \")], -1 /* HOISTED */);\nconst _hoisted_4 = {\n class: \"player-bar media\"\n};\nconst _hoisted_5 = {\n class: \"media-left\"\n};\nconst _hoisted_6 = [\"title\", \"aria-label\"];\nconst _hoisted_7 = {\n key: 0,\n class: \"fas fa-pause\"\n};\nconst _hoisted_8 = {\n key: 1,\n class: \"fas fa-play\"\n};\nconst _hoisted_9 = {\n key: 0,\n class: \"media-left media-cover\"\n};\nconst _hoisted_10 = [\"src\"];\nconst _hoisted_11 = {\n class: \"media-content\"\n};\nconst _hoisted_12 = {\n class: \"media-right\"\n};\nconst _hoisted_13 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon is-size-6 has-text-danger\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-circle\"\n})], -1 /* HOISTED */);\nconst _hoisted_14 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"Live\", -1 /* HOISTED */);\nconst _hoisted_15 = [_hoisted_13, _hoisted_14];\nconst _hoisted_16 = {\n key: 0,\n class: \"is-size-6\"\n};\nconst _hoisted_17 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-thumbtack\"\n})], -1 /* HOISTED */);\nconst _hoisted_18 = {\n key: 0,\n class: \"is-size-6\"\n};\nconst _hoisted_19 = /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"icon\"\n}, [/*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"fa fa-list\"\n})], -1 /* HOISTED */);\n\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n const _component_APlaylist = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"APlaylist\");\n const _component_AProgress = (0,vue__WEBPACK_IMPORTED_MODULE_0__.resolveComponent)(\"AProgress\");\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)(['player-panels', $data.panel ? 'is-open' : ''])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_APlaylist, {\n ref: \"pin\",\n class: \"player-panel menu\",\n name: \"Pinned\",\n actions: ['page'],\n editable: true,\n player: $options.self,\n set: $data.sets.pin,\n onSelect: _cache[0] || (_cache[0] = $event => $options.togglePlay('pin', $event.index)),\n listClass: \"menu-list\",\n itemClass: \"menu-item\"\n }, {\n header: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withCtx)(() => [_hoisted_2]),\n _: 1 /* STABLE */\n }, 8 /* PROPS */, [\"player\", \"set\"]), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, $data.panel == 'pin']]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)(_component_APlaylist, {\n ref: \"queue\",\n class: \"player-panel menu\",\n actions: ['page'],\n editable: true,\n player: $options.self,\n set: $data.sets.queue,\n onSelect: _cache[1] || (_cache[1] = $event => $options.togglePlay('queue', $event.index)),\n listClass: \"menu-list\",\n itemClass: \"menu-item\"\n }, {\n header: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withCtx)(() => [_hoisted_3]),\n _: 1 /* STABLE */\n }, 8 /* PROPS */, [\"player\", \"set\"]), [[vue__WEBPACK_IMPORTED_MODULE_0__.vShow, $data.panel == 'queue']])], 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_4, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_5, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"button\",\n onClick: _cache[2] || (_cache[2] = $event => $options.togglePlay()),\n title: $props.buttonTitle,\n \"aria-label\": $props.buttonTitle\n }, [$options.playing ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_7)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_8))], 8 /* PROPS */, _hoisted_6)]), $options.current && $options.current.data.cover ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"img\", {\n src: $options.current.data.cover,\n class: \"cover\"\n }, null, 8 /* PROPS */, _hoisted_10)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_11, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.renderSlot)(_ctx.$slots, \"content\", {\n loaded: $data.loaded,\n live: $data.live,\n current: $options.current\n }), $data.loaded && $data.duration ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_AProgress, {\n key: 0,\n value: $data.currentTime,\n max: this.duration,\n format: $options.displayTime,\n class: \"pt-1 is-size-7\",\n onSelect: _cache[3] || (_cache[3] = $event => $data.audio.currentTime = $event)\n }, null, 8 /* PROPS */, [\"value\", \"max\", \"format\"])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_12, [$data.loaded ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 0,\n class: \"button has-text-weight-bold\",\n onClick: _cache[4] || (_cache[4] = $event => $options.play())\n }, _hoisted_15)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n ref: \"pinPlaylistButton\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)($options.playlistButtonClass('pin')),\n onClick: _cache[5] || (_cache[5] = $event => $options.togglePanel('pin'))\n }, [$data.sets.pin.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($data.sets.pin.length), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), _hoisted_17], 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)($options.playlistButtonClass('queue')),\n onClick: _cache[6] || (_cache[6] = $event => $options.togglePanel('queue'))\n }, [$data.sets.queue.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_18, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($data.sets.queue.length), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), _hoisted_19], 2 /* CLASS */)])])]);\n}\n\n//# sourceURL=webpack://aircox-assets/./src/components/APlayer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D");
/***/ }),
diff --git a/aircox/templates/aircox/page_detail.html b/aircox/templates/aircox/page_detail.html
index 8dc4e2b..5aa296c 100644
--- a/aircox/templates/aircox/page_detail.html
+++ b/aircox/templates/aircox/page_detail.html
@@ -5,12 +5,13 @@ Base template used to display a Page
Context:
- page: page
+- parent: parent page
{% endcomment %}
{% block header_crumbs %}
{{ block.super }}
{% if page.category %}
-/ {{ page.category.title }}
+{% if parent %} / {% endif %} {{ page.category.title }}
{% endif %}
{% endblock %}
diff --git a/aircox/templates/aircox/widgets/player.html b/aircox/templates/aircox/widgets/player.html
index b7d8c02..aa2584c 100644
--- a/aircox/templates/aircox/widgets/player.html
+++ b/aircox/templates/aircox/widgets/player.html
@@ -9,8 +9,9 @@ The audio player
role="{% translate "player" %}"
aria-description="{% translate "Audio player used to listen to the radio and podcasts" %}">
diff --git a/aircox/templatetags/aircox.py b/aircox/templatetags/aircox.py
index e1d9aba..8430094 100644
--- a/aircox/templatetags/aircox.py
+++ b/aircox/templatetags/aircox.py
@@ -53,6 +53,16 @@ def do_json(obj, fields=""):
return json.dumps(obj)
+@register.simple_tag(name='player_live_attr', takes_context=True)
+def do_player_live_attr(context):
+ """ Player 'live-args' attribute value """
+ station = getattr(context['request'], 'station', None)
+ return json.dumps({
+ 'url': reverse('api:live'),
+ 'src': station and station.audio_streams.split('\n')
+ })
+
+
@register.simple_tag(name='nav_items', takes_context=True)
def do_nav_items(context, menu, **kwargs):
""" Render navigation items for the provided menu name. """
diff --git a/aircox/views/base.py b/aircox/views/base.py
index dc6f3fa..74b1a6c 100644
--- a/aircox/views/base.py
+++ b/aircox/views/base.py
@@ -1,14 +1,10 @@
-
-from django.http import Http404
-from django.views.generic import DetailView, ListView
from django.views.generic.base import TemplateResponseMixin, ContextMixin
from django.urls import reverse
from ..models import Page
-from ..utils import Redirect
-__all__ = ['BaseView']
+__all__ = ('BaseView', 'BaseAPIView')
class BaseView(TemplateResponseMixin, ContextMixin):
@@ -55,8 +51,8 @@ class BaseView(TemplateResponseMixin, ContextMixin):
kwargs['audio_streams'] = streams
if 'model' not in kwargs:
- model = getattr(self, 'model', None) or hasattr(self, 'object') and \
- type(self.object)
+ model = getattr(self, 'model', None) or \
+ hasattr(self, 'object') and type(self.object)
kwargs['model'] = model
return super().get_context_data(**kwargs)
diff --git a/assets/src/assets/styles.scss b/assets/src/assets/styles.scss
index d561aac..16abdba 100644
--- a/assets/src/assets/styles.scss
+++ b/assets/src/assets/styles.scss
@@ -229,6 +229,10 @@ a.navbar-item.is-active {
.player-bar {
border-top: 1px $grey-light solid;
+ > div {
+ height: 3.75em !important;
+ }
+
> .media-left:not(:last-child) {
margin-right: 0em;
}
@@ -249,7 +253,8 @@ a.navbar-item.is-active {
.button {
font-size: 1.5rem !important;
- height: 2.5em;
+ height: 100%;
+ padding: auto 0.2em !important;
min-width: 2.5em;
border-radius: 0px;
transition: background-color 1s;
diff --git a/assets/src/components/APlayer.vue b/assets/src/components/APlayer.vue
index 4312e9a..a94500a 100644
--- a/assets/src/components/APlayer.vue
+++ b/assets/src/components/APlayer.vue
@@ -52,13 +52,13 @@
@@ -186,13 +186,11 @@ export default {
if(!item)
throw `No sound at index ${index} for playlist ${playlist}`;
this.loaded = item
- this.current = item
src = item.src;
}
// from live
else {
this.loaded = null;
- this.current = this.live.current
src = this.live.src;
}