Fix template refs, rebuild NP components.
This commit is contained in:
parent
6cb6236fa7
commit
8259989f6a
|
@ -6,15 +6,15 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {get, templateRef} from "@vueuse/core";
|
||||
import {ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
lastOutput: String,
|
||||
});
|
||||
|
||||
const $modal = templateRef('modal');
|
||||
const modal = ref(); // Template ref
|
||||
const show = () => {
|
||||
get($modal).show();
|
||||
modal.value.show();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -67,15 +67,15 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {get, templateRef} from "@vueuse/core";
|
||||
import {ref} from "vue";
|
||||
|
||||
const $modal = templateRef('modal');
|
||||
const modal = ref();
|
||||
|
||||
const create = () => {
|
||||
get($modal).show();
|
||||
modal.value.show();
|
||||
}
|
||||
const close = () => {
|
||||
get($modal).hide();
|
||||
modal.value.hide();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -39,15 +39,15 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {get, templateRef} from "@vueuse/core";
|
||||
import {ref} from "vue";
|
||||
|
||||
const $modal = templateRef('modal');
|
||||
const modal = ref(); // Template ref
|
||||
|
||||
const create = () => {
|
||||
get($modal).show();
|
||||
modal.value.show();
|
||||
}
|
||||
const close = () => {
|
||||
get($modal).hide();
|
||||
modal.value.hide();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -5,14 +5,21 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import store from 'store';
|
||||
import getLogarithmicVolume from '~/functions/getLogarithmicVolume.js';
|
||||
import Hls from 'hls.js';
|
||||
import {usePlayerStore} from "~/store.js";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: String
|
||||
title: String,
|
||||
volume: {
|
||||
type: Number,
|
||||
default: 55
|
||||
},
|
||||
isMuted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
|
@ -23,7 +30,6 @@ export default {
|
|||
return {
|
||||
'audio': null,
|
||||
'hls': null,
|
||||
'volume': 55,
|
||||
'duration': 0,
|
||||
'currentTime': 0
|
||||
};
|
||||
|
@ -41,9 +47,10 @@ export default {
|
|||
if (this.audio !== null) {
|
||||
this.audio.volume = getLogarithmicVolume(volume);
|
||||
}
|
||||
|
||||
if (store.enabled) {
|
||||
store.set('player_volume', volume);
|
||||
},
|
||||
isMuted(muted) {
|
||||
if (this.audio !== null) {
|
||||
this.audio.muted = muted;
|
||||
}
|
||||
},
|
||||
current(newCurrent) {
|
||||
|
@ -62,19 +69,6 @@ export default {
|
|||
this.stop();
|
||||
});
|
||||
}
|
||||
|
||||
// Check webstorage for existing volume preference.
|
||||
if (store.enabled && store.get('player_volume') !== undefined) {
|
||||
this.volume = store.get('player_volume', this.volume);
|
||||
}
|
||||
|
||||
// Check the query string if browser supports easy query string access.
|
||||
if (typeof URLSearchParams !== 'undefined') {
|
||||
let urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('volume')) {
|
||||
this.volume = parseInt(urlParams.get('volume'));
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stop() {
|
||||
|
@ -82,6 +76,7 @@ export default {
|
|||
this.audio.pause();
|
||||
this.audio.src = '';
|
||||
}
|
||||
|
||||
if (this.hls !== null) {
|
||||
this.hls.destroy();
|
||||
this.hls = null;
|
||||
|
@ -126,6 +121,7 @@ export default {
|
|||
};
|
||||
|
||||
this.audio.volume = getLogarithmicVolume(this.volume);
|
||||
this.audio.muted = this.isMuted;
|
||||
|
||||
if (this.current.isHls) {
|
||||
// HLS playback support
|
||||
|
@ -160,12 +156,6 @@ export default {
|
|||
isHls: isHls,
|
||||
});
|
||||
},
|
||||
getVolume() {
|
||||
return this.volume;
|
||||
},
|
||||
setVolume(vol) {
|
||||
this.volume = vol;
|
||||
},
|
||||
getCurrentTime() {
|
||||
return this.currentTime;
|
||||
},
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'IsMounted',
|
||||
data() {
|
||||
return {
|
||||
isMounted: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isMounted = true;
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,87 @@
|
|||
import NowPlaying from '~/components/Entity/NowPlaying';
|
||||
import {onMounted, shallowRef, watch} from "vue";
|
||||
import {set, useEventSource} from "@vueuse/core";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
|
||||
export const nowPlayingProps = {
|
||||
nowPlayingUri: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
useSse: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
sseUri: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
initialNowPlaying: {
|
||||
type: Object,
|
||||
default() {
|
||||
return NowPlaying;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function useNowPlaying(props) {
|
||||
const np = shallowRef(props.initialNowPlaying);
|
||||
|
||||
const setNowPlaying = (np_new) => {
|
||||
set(np, np_new);
|
||||
|
||||
// Update the browser metadata for browsers that support it (i.e. Mobile Chrome)
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: np_new.now_playing.song.title,
|
||||
artist: np_new.now_playing.song.artist,
|
||||
artwork: [
|
||||
{src: np_new.now_playing.song.art}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent("now-playing", {
|
||||
detail: np_new
|
||||
}));
|
||||
}
|
||||
|
||||
if (props.useSse) {
|
||||
const {data} = useEventSource(props.sseUri);
|
||||
watch(data, (sse_data_raw) => {
|
||||
const sse_data = JSON.parse(sse_data_raw);
|
||||
const sse_np = sse_data?.pub?.data?.np || null;
|
||||
|
||||
if (sse_np) {
|
||||
setTimeout(() => {
|
||||
setNowPlaying(sse_np);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const {axios} = useAxios();
|
||||
const checkNowPlaying = () => {
|
||||
axios.get(props.nowPlayingUri, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
}
|
||||
}).then((response) => {
|
||||
setNowPlaying(response.data);
|
||||
|
||||
setTimeout(checkNowPlaying, (!document.hidden) ? 15000 : 30000);
|
||||
}).catch(() => {
|
||||
setTimeout(checkNowPlaying, (!document.hidden) ? 30000 : 120000);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(checkNowPlaying, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
return np;
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
<template></template>
|
||||
<script>
|
||||
import NowPlaying from '~/components/Entity/NowPlaying';
|
||||
|
||||
export const nowPlayingProps = {
|
||||
props: {
|
||||
nowPlayingUri: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
useSse: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
sseUri: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
initialNowPlaying: {
|
||||
type: Object,
|
||||
default() {
|
||||
return NowPlaying;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [nowPlayingProps],
|
||||
data() {
|
||||
return {
|
||||
'sse': null
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
// Convert initial NP data from prop to data.
|
||||
this.setNowPlaying(this.initialNowPlaying);
|
||||
|
||||
setTimeout(this.checkNowPlaying, 5000);
|
||||
},
|
||||
methods: {
|
||||
checkNowPlaying() {
|
||||
if (this.useSse) {
|
||||
this.sse = new EventSource(this.sseUri);
|
||||
|
||||
this.sse.onopen = (e) => {
|
||||
console.log(e);
|
||||
};
|
||||
|
||||
this.sse.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const np = data?.pub?.data?.np || null;
|
||||
if (np) {
|
||||
setTimeout(() => {
|
||||
this.setNowPlaying(np);
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
this.axios.get(this.nowPlayingUri, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0',
|
||||
}
|
||||
}).then((response) => {
|
||||
this.setNowPlaying(response.data);
|
||||
|
||||
setTimeout(this.checkNowPlaying, (!document.hidden) ? 15000 : 30000);
|
||||
}).catch(() => {
|
||||
setTimeout(this.checkNowPlaying, (!document.hidden) ? 30000 : 120000);
|
||||
});
|
||||
}
|
||||
},
|
||||
setNowPlaying(np_new) {
|
||||
// Update the browser metadata for browsers that support it (i.e. Mobile Chrome)
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: np_new.now_playing.song.title,
|
||||
artist: np_new.now_playing.song.artist,
|
||||
artwork: [
|
||||
{src: np_new.now_playing.song.art}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('np_updated', np_new);
|
||||
|
||||
document.dispatchEvent(new CustomEvent("now-playing", {
|
||||
detail: np_new
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -5,10 +5,9 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {get, templateRef, watchOnce} from "@vueuse/core";
|
||||
import {Tableau20} from "~/vendor/chartjs-colorschemes/colorschemes.tableau";
|
||||
import {Chart} from "chart.js";
|
||||
import {onUnmounted} from "vue";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
options: Object,
|
||||
|
@ -20,10 +19,10 @@ const props = defineProps({
|
|||
}
|
||||
});
|
||||
|
||||
const $canvas = templateRef('canvas');
|
||||
const canvas = ref(); // Template ref
|
||||
let $chart = null;
|
||||
|
||||
watchOnce($canvas, () => {
|
||||
onMounted(() => {
|
||||
const defaultOptions = {
|
||||
type: 'pie',
|
||||
data: {
|
||||
|
@ -45,7 +44,7 @@ watchOnce($canvas, () => {
|
|||
}
|
||||
|
||||
let chartOptions = _.defaultsDeep({}, props.options, defaultOptions);
|
||||
$chart = new Chart(get($canvas).getContext('2d'), chartOptions);
|
||||
$chart = new Chart(canvas.value.getContext('2d'), chartOptions);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<script setup>
|
||||
import Icon from "./Icon";
|
||||
import {usePlayerStore} from "~/store";
|
||||
import {computed} from "vue";
|
||||
import {computed, toRef} from "vue";
|
||||
import {get} from "@vueuse/core";
|
||||
import gettext from "~/vendor/gettext";
|
||||
import getUrlWithoutQuery from "~/functions/getUrlWithoutQuery";
|
||||
|
@ -26,15 +26,9 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const $store = usePlayerStore();
|
||||
const {$gettext} = gettext;
|
||||
|
||||
const isPlaying = computed(() => {
|
||||
return $store.isPlaying;
|
||||
});
|
||||
|
||||
const current = computed(() => {
|
||||
return $store.current;
|
||||
});
|
||||
const isPlaying = toRef($store, 'isPlaying');
|
||||
const current = toRef($store, 'current');
|
||||
|
||||
const isThisPlaying = computed(() => {
|
||||
if (!get(isPlaying)) {
|
||||
|
@ -46,6 +40,8 @@ const isThisPlaying = computed(() => {
|
|||
return playingUrl === thisUrl;
|
||||
});
|
||||
|
||||
const {$gettext} = gettext;
|
||||
|
||||
const langTitle = computed(() => {
|
||||
return get(isThisPlaying)
|
||||
? $gettext('Stop')
|
||||
|
|
|
@ -17,29 +17,29 @@
|
|||
<script setup>
|
||||
import StreamingLogView from "~/components/Common/StreamingLogView";
|
||||
import {ref} from "vue";
|
||||
import {get, set, templateRef, useClipboard} from "@vueuse/core";
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
|
||||
const logUrl = ref('');
|
||||
const $modal = templateRef('modal');
|
||||
const $logView = templateRef('logView');
|
||||
const modal = ref(); // Template ref
|
||||
const logView = ref(); // Template ref
|
||||
|
||||
const show = (newLogUrl) => {
|
||||
set(logUrl, newLogUrl);
|
||||
get($modal).show();
|
||||
logUrl.value = newLogUrl;
|
||||
modal.value.show();
|
||||
};
|
||||
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const doCopy = () => {
|
||||
clipboard.copy(get($logView).getContents());
|
||||
clipboard.copy(logView.value.getContents());
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
get($modal).hide();
|
||||
modal.value.hide();
|
||||
}
|
||||
|
||||
const clearContents = () => {
|
||||
set(logUrl, '');
|
||||
logUrl.value = '';
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {get, templateRef, watchOnce} from "@vueuse/core";
|
||||
import {get} from "@vueuse/core";
|
||||
import {Tableau20} from "~/vendor/chartjs-colorschemes/colorschemes.tableau";
|
||||
import {DateTime} from "luxon";
|
||||
import _ from "lodash";
|
||||
import {Chart} from "chart.js";
|
||||
import {onUnmounted} from "vue";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import gettext from "~/vendor/gettext";
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -18,12 +18,12 @@ const props = defineProps({
|
|||
data: Array
|
||||
});
|
||||
|
||||
const $canvas = templateRef('canvas');
|
||||
const canvas = ref(); // Template ref
|
||||
let $chart = null;
|
||||
|
||||
const {$gettext} = gettext;
|
||||
|
||||
watchOnce($canvas, () => {
|
||||
onMounted(() => {
|
||||
const defaultOptions = {
|
||||
type: 'line',
|
||||
data: {
|
||||
|
@ -92,7 +92,7 @@ watchOnce($canvas, () => {
|
|||
}
|
||||
|
||||
let chartOptions = _.defaultsDeep({}, props.options, defaultOptions);
|
||||
$chart = new Chart(get($canvas).getContext('2d'), chartOptions);
|
||||
$chart = new Chart(get(canvas).getContext('2d'), chartOptions);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div style="display: contents">
|
||||
<audio-player ref="player"></audio-player>
|
||||
<audio-player ref="player" :volume="volume" :is-muted="isMuted"></audio-player>
|
||||
|
||||
<div class="ml-3 player-inline" v-if="isPlaying">
|
||||
<div class="inline-seek d-inline-flex align-items-center ml-1" v-if="!current.isStream && duration !== 0">
|
||||
|
@ -8,7 +8,8 @@
|
|||
{{ currentTimeText }}
|
||||
</div>
|
||||
<div class="flex-fill mx-2">
|
||||
<input type="range" :title="langSeek" class="player-seek-range custom-range" min="0" max="100"
|
||||
<input type="range" :title="$gettext('Seek')" class="player-seek-range custom-range" min="0"
|
||||
max="100"
|
||||
step="1" v-model="progress">
|
||||
</div>
|
||||
<div class="flex-shrink-0 mx-1 text-white-50 time-display">
|
||||
|
@ -28,7 +29,8 @@
|
|||
</a>
|
||||
</div>
|
||||
<div class="flex-fill mx-1">
|
||||
<input type="range" :title="langVolume" class="player-volume-range custom-range" min="0" max="100"
|
||||
<input type="range" :title="$gettext('Volume')" class="player-volume-range custom-range" min="0"
|
||||
max="100"
|
||||
step="1" v-model="volume">
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
|
@ -64,92 +66,69 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import AudioPlayer from '~/components/Common/AudioPlayer';
|
||||
import formatTime from '~/functions/formatTime.js';
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import {usePlayerStore} from "~/store.js";
|
||||
import {get, set, useMounted, useStorage} from "@vueuse/core";
|
||||
import {computed, ref, toRef} from "vue";
|
||||
|
||||
export default {
|
||||
components: {Icon, AudioPlayer},
|
||||
setup() {
|
||||
return {
|
||||
store: usePlayerStore()
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
is_mounted: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.is_mounted = true;
|
||||
},
|
||||
computed: {
|
||||
langSeek() {
|
||||
return this.$gettext('Seek');
|
||||
},
|
||||
langVolume () {
|
||||
return this.$gettext('Volume');
|
||||
},
|
||||
durationText () {
|
||||
return formatTime(this.duration);
|
||||
},
|
||||
currentTimeText () {
|
||||
return formatTime(this.currentTime);
|
||||
},
|
||||
duration () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
const store = usePlayerStore();
|
||||
const isPlaying = toRef(store, 'isPlaying');
|
||||
const current = toRef(store, 'current');
|
||||
|
||||
return this.$refs.player.getDuration();
|
||||
},
|
||||
currentTime () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
const volume = useStorage('player_volume', 55);
|
||||
const isMuted = useStorage('player_is_muted', false);
|
||||
const isMounted = useMounted();
|
||||
const player = ref(); // Template ref
|
||||
|
||||
return this.$refs.player.getCurrentTime();
|
||||
},
|
||||
volume: {
|
||||
get () {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.getVolume();
|
||||
},
|
||||
set (vol) {
|
||||
this.$refs.player.setVolume(vol);
|
||||
}
|
||||
},
|
||||
progress: {
|
||||
get() {
|
||||
if (!this.is_mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.getProgress();
|
||||
},
|
||||
set(progress) {
|
||||
this.$refs.player.setProgress(progress);
|
||||
}
|
||||
},
|
||||
isPlaying() {
|
||||
return this.store.isPlaying;
|
||||
},
|
||||
current() {
|
||||
return this.store.current;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
stop () {
|
||||
this.store.toggle({
|
||||
url: null,
|
||||
isStream: true
|
||||
});
|
||||
}
|
||||
const duration = computed(() => {
|
||||
if (!get(isMounted)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return get(player).getDuration();
|
||||
});
|
||||
|
||||
const durationText = computed(() => {
|
||||
return formatTime(get(duration));
|
||||
});
|
||||
|
||||
const currentTime = computed(() => {
|
||||
if (!get(isMounted)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return get(player).getCurrentTime();
|
||||
});
|
||||
|
||||
const currentTimeText = computed(() => {
|
||||
return formatTime(get(currentTime));
|
||||
});
|
||||
|
||||
const progress = computed({
|
||||
get: () => {
|
||||
if (!get(isMounted)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return get(player).getProgress();
|
||||
},
|
||||
set: (prog) => {
|
||||
get(player).setProgress(prog);
|
||||
}
|
||||
});
|
||||
|
||||
const stop = () => {
|
||||
store.toggle({
|
||||
url: null,
|
||||
isStream: true,
|
||||
isHls: false,
|
||||
});
|
||||
};
|
||||
|
||||
const mute = () => {
|
||||
set(isMuted, !get(isMuted));
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -13,15 +13,15 @@
|
|||
<div class="card-actions">
|
||||
<a class="btn btn-sm btn-outline-secondary" v-b-modal.song_history_modal>
|
||||
<icon icon="history"></icon>
|
||||
{{ langSongHistory }}
|
||||
{{ $gettext('Song History') }}
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" v-if="enableRequests" v-b-modal.request_modal>
|
||||
<icon icon="help_outline"></icon>
|
||||
{{ langRequestSong }}
|
||||
{{ $gettext('Request Song') }}
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" :href="downloadPlaylistUri">
|
||||
<icon icon="file_download"></icon>
|
||||
{{ langDownloadPlaylist }}
|
||||
{{ $gettext('Playlist') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,54 +33,45 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RadioPlayer, {radioPlayerProps} from './Player.vue';
|
||||
<script setup>
|
||||
import SongHistoryModal from './FullPlayer/SongHistoryModal';
|
||||
import RequestModal from './FullPlayer/RequestModal';
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import RadioPlayer, {radioPlayerProps} from './Player.vue';
|
||||
import {useMounted} from "@vueuse/core";
|
||||
import {ref} from "vue";
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
components: { Icon, RequestModal, SongHistoryModal, RadioPlayer },
|
||||
mixins: [radioPlayerProps],
|
||||
props: {
|
||||
stationName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
enableRequests: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
downloadPlaylistUri: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
requestListUri: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
const props = defineProps({
|
||||
...radioPlayerProps,
|
||||
stationName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
computed: {
|
||||
langSongHistory () {
|
||||
return this.$gettext('Song History');
|
||||
},
|
||||
langRequestSong () {
|
||||
return this.$gettext('Request Song');
|
||||
},
|
||||
langDownloadPlaylist () {
|
||||
return this.$gettext('Playlist');
|
||||
}
|
||||
enableRequests: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
methods: {
|
||||
onNowPlayingUpdate (newNowPlaying) {
|
||||
this.$refs.history_modal.updateHistory(newNowPlaying);
|
||||
}
|
||||
downloadPlaylistUri: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
requestListUri: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const history_modal = ref(); // Template ref
|
||||
const isMounted = useMounted();
|
||||
|
||||
const onNowPlayingUpdate = (newNowPlaying) => {
|
||||
if (isMounted.value) {
|
||||
history_modal.value.updateHistory(newNowPlaying);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,32 +1,25 @@
|
|||
<template>
|
||||
<div id="song_history">
|
||||
<now-playing v-bind="$props" @np_updated="setNowPlaying"></now-playing>
|
||||
<song-history :show-album-art="showAlbumArt" :history="history"></song-history>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SongHistory from './FullPlayer/SongHistory';
|
||||
import NowPlaying, {nowPlayingProps} from '~/components/Common/NowPlaying';
|
||||
<script setup>
|
||||
import useNowPlaying, {nowPlayingProps} from '~/components/Common/NowPlaying';
|
||||
import {computed} from "vue";
|
||||
import SongHistory from "~/components/Public/FullPlayer/SongHistory.vue";
|
||||
|
||||
export default {
|
||||
mixins: [nowPlayingProps],
|
||||
components: {NowPlaying, SongHistory},
|
||||
props: {
|
||||
showAlbumArt: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
const props = defineProps({
|
||||
...nowPlayingProps,
|
||||
showAlbumArt: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
history: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setNowPlaying(np) {
|
||||
this.history = np.song_history;
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const np = useNowPlaying(props);
|
||||
|
||||
const history = computed(() => {
|
||||
return np.value.song_history ?? [];
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div class="radio-player-widget">
|
||||
<now-playing v-bind="$props" @np_updated="setNowPlaying"></now-playing>
|
||||
<audio-player ref="player" v-bind:title="np.now_playing.song.text"></audio-player>
|
||||
<audio-player ref="player" :title="np.now_playing.song.text" :volume="volume"
|
||||
:is-muted="isMuted"></audio-player>
|
||||
|
||||
<div class="now-playing-details">
|
||||
<div class="now-playing-art" v-if="showAlbumArt && np.now_playing.song.art">
|
||||
<a v-bind:href="np.now_playing.song.art" data-fancybox target="_blank">
|
||||
<img v-bind:src="np.now_playing.song.art" :alt="lang_album_art_alt">
|
||||
<a :href="np.now_playing.song.art" data-fancybox target="_blank">
|
||||
<img :src="np.now_playing.song.art" :alt="$gettext('Album Art')">
|
||||
</a>
|
||||
</div>
|
||||
<div class="now-playing-main">
|
||||
|
@ -28,14 +28,14 @@
|
|||
<h4 class="now-playing-title">{{ np.now_playing.song.text }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="time-display" v-if="time_display_played">
|
||||
<div class="time-display" v-if="time_display_played != null">
|
||||
<div class="time-display-played text-secondary">
|
||||
{{ time_display_played }}
|
||||
</div>
|
||||
<div class="time-display-progress">
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-secondary" role="progressbar"
|
||||
v-bind:style="{ width: time_percent+'%' }"></div>
|
||||
:style="{ width: time_percent+'%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-display-total text-secondary">
|
||||
|
@ -52,7 +52,7 @@
|
|||
:is-hls="current_stream.hls" is-stream></play-button>
|
||||
|
||||
<div class="radio-control-select-stream">
|
||||
<div v-if="this.streams.length > 1" class="dropdown">
|
||||
<div v-if="streams.length > 1" class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="btn-select-stream"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ current_stream.name }}
|
||||
|
@ -67,16 +67,16 @@
|
|||
</div>
|
||||
|
||||
<div class="radio-control-mute-button">
|
||||
<a href="#" class="text-secondary" :title="lang_mute_btn" @click.prevent="volume = 0">
|
||||
<a href="#" class="text-secondary" :title="$gettext('Mute')" @click.prevent="toggleMute">
|
||||
<icon icon="volume_mute"></icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="radio-control-volume-slider">
|
||||
<input type="range" :title="lang_volume_slider" class="custom-range" min="0" max="100" step="1"
|
||||
v-model="volume">
|
||||
<input type="range" :title="$gettext('Volume')" class="custom-range" min="0" max="100" step="1"
|
||||
:disabled="isMuted" v-model.number="volume">
|
||||
</div>
|
||||
<div class="radio-control-max-volume-button">
|
||||
<a href="#" class="text-secondary" :title="lang_full_volume_btn" @click.prevent="volume = 100">
|
||||
<a href="#" class="text-secondary" :title="$gettext('Full Volume')" @click.prevent="fullVolume">
|
||||
<icon icon="volume_up"></icon>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -208,202 +208,201 @@
|
|||
</style>
|
||||
|
||||
<script>
|
||||
import AudioPlayer from '~/components/Common/AudioPlayer';
|
||||
import NowPlaying, {nowPlayingProps} from '~/components/Common/NowPlaying';
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import PlayButton from "~/components/Common/PlayButton";
|
||||
import IsMounted from "~/components/Common/IsMounted";
|
||||
import {nowPlayingProps} from '~/components/Common/NowPlaying.js';
|
||||
|
||||
export const radioPlayerProps = {
|
||||
...nowPlayingProps,
|
||||
props: {
|
||||
...nowPlayingProps.props,
|
||||
showHls: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
hlsIsDefault: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showAlbumArt: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {PlayButton, Icon, NowPlaying, AudioPlayer},
|
||||
mixins: [radioPlayerProps, IsMounted],
|
||||
data() {
|
||||
return {
|
||||
'np': this.initialNowPlaying,
|
||||
'np_elapsed': 0,
|
||||
'current_stream': {
|
||||
'name': '',
|
||||
'url': '',
|
||||
'hls': false,
|
||||
},
|
||||
'clock_interval': null
|
||||
};
|
||||
showHls: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
mounted() {
|
||||
this.clock_interval = setInterval(this.iterateTimer, 1000);
|
||||
|
||||
if (this.autoplay) {
|
||||
this.switchStream(this.current_stream);
|
||||
}
|
||||
hlsIsDefault: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
computed: {
|
||||
lang_mute_btn() {
|
||||
return this.$gettext('Mute');
|
||||
},
|
||||
lang_volume_slider() {
|
||||
return this.$gettext('Volume');
|
||||
},
|
||||
lang_full_volume_btn() {
|
||||
return this.$gettext('Full Volume');
|
||||
},
|
||||
lang_album_art_alt() {
|
||||
return this.$gettext('Album Art');
|
||||
},
|
||||
streams() {
|
||||
let all_streams = [];
|
||||
|
||||
if (this.enable_hls) {
|
||||
all_streams.push({
|
||||
'name': this.$gettext('HLS'),
|
||||
'url': this.np.station.hls_url,
|
||||
'hls': true,
|
||||
});
|
||||
}
|
||||
|
||||
this.np.station.mounts.forEach(function (mount) {
|
||||
all_streams.push({
|
||||
'name': mount.name,
|
||||
'url': mount.url,
|
||||
'hls': false,
|
||||
});
|
||||
});
|
||||
this.np.station.remotes.forEach(function (remote) {
|
||||
all_streams.push({
|
||||
'name': remote.name,
|
||||
'url': remote.url,
|
||||
'hls': false,
|
||||
});
|
||||
});
|
||||
return all_streams;
|
||||
},
|
||||
enable_hls() {
|
||||
return this.showHls && this.np.station.hls_enabled;
|
||||
},
|
||||
time_percent() {
|
||||
let time_played = this.np_elapsed;
|
||||
let time_total = this.np.now_playing.duration;
|
||||
|
||||
if (!time_total) {
|
||||
return 0;
|
||||
}
|
||||
if (time_played > time_total) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return (time_played / time_total) * 100;
|
||||
},
|
||||
time_display_played() {
|
||||
let time_played = this.np_elapsed;
|
||||
let time_total = this.np.now_playing.duration;
|
||||
|
||||
if (!time_total) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (time_played > time_total) {
|
||||
time_played = time_total;
|
||||
}
|
||||
|
||||
return this.formatTime(time_played);
|
||||
},
|
||||
time_display_total() {
|
||||
let time_total = this.np.now_playing.duration;
|
||||
return (time_total) ? this.formatTime(time_total) : null;
|
||||
},
|
||||
volume: {
|
||||
get() {
|
||||
if (!this.isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.$refs.player.getVolume();
|
||||
},
|
||||
set(vol) {
|
||||
this.$refs.player.setVolume(vol);
|
||||
}
|
||||
}
|
||||
showAlbumArt: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
methods: {
|
||||
switchStream(new_stream) {
|
||||
this.current_stream = new_stream;
|
||||
this.$refs.player.toggle(this.current_stream.url, true, this.current_stream.hls);
|
||||
},
|
||||
setNowPlaying(np_new) {
|
||||
this.np = np_new;
|
||||
this.$emit('np_updated', np_new);
|
||||
|
||||
// Set a "default" current stream if none exists.
|
||||
if (this.current_stream.url === '' && this.streams.length > 0) {
|
||||
if (this.hlsIsDefault && this.enable_hls) {
|
||||
this.current_stream = this.streams[0];
|
||||
} else {
|
||||
let current_stream = null;
|
||||
if (np_new.station.listen_url !== '') {
|
||||
this.streams.forEach(function (stream) {
|
||||
if (stream.url === np_new.station.listen_url) {
|
||||
current_stream = stream;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (current_stream === null) {
|
||||
current_stream = this.streams[0];
|
||||
}
|
||||
this.current_stream = current_stream;
|
||||
}
|
||||
}
|
||||
},
|
||||
iterateTimer() {
|
||||
let current_time = Math.floor(Date.now() / 1000);
|
||||
let np_elapsed = current_time - this.np.now_playing.played_at;
|
||||
if (np_elapsed < 0) {
|
||||
np_elapsed = 0;
|
||||
} else if (np_elapsed >= this.np.now_playing.duration) {
|
||||
np_elapsed = this.np.now_playing.duration;
|
||||
}
|
||||
this.np_elapsed = np_elapsed;
|
||||
},
|
||||
formatTime(time) {
|
||||
let sec_num = parseInt(time, 10);
|
||||
|
||||
let hours = Math.floor(sec_num / 3600);
|
||||
let minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
let seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if (hours < 10) {
|
||||
hours = '0' + hours;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
minutes = '0' + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = '0' + seconds;
|
||||
}
|
||||
return (hours !== '00' ? hours + ':' : '') + minutes + ':' + seconds;
|
||||
}
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<script setup>
|
||||
import AudioPlayer from '~/components/Common/AudioPlayer';
|
||||
import Icon from '~/components/Common/Icon';
|
||||
import PlayButton from "~/components/Common/PlayButton";
|
||||
import {computed, onMounted, ref, shallowRef, watch} from "vue";
|
||||
import {useIntervalFn, useMounted, useStorage} from "@vueuse/core";
|
||||
import formatTime from "~/functions/formatTime";
|
||||
import gettext from "~/vendor/gettext";
|
||||
import useNowPlaying from "~/components/Common/NowPlaying.js";
|
||||
|
||||
const props = defineProps({
|
||||
...radioPlayerProps
|
||||
});
|
||||
|
||||
const emit = defineEmits(['np_updated']);
|
||||
|
||||
const np = useNowPlaying(props);
|
||||
const np_elapsed = ref(0);
|
||||
const current_stream = shallowRef({
|
||||
'name': '',
|
||||
'url': '',
|
||||
'hls': false,
|
||||
});
|
||||
|
||||
const enable_hls = computed(() => {
|
||||
let $np = np.value;
|
||||
return props.showHls && $np.station.hls_enabled;
|
||||
});
|
||||
|
||||
const {$gettext} = gettext;
|
||||
|
||||
const streams = computed(() => {
|
||||
let all_streams = [];
|
||||
let $np = np.value;
|
||||
|
||||
if (enable_hls.value) {
|
||||
all_streams.push({
|
||||
'name': $gettext('HLS'),
|
||||
'url': $np.station.hls_url,
|
||||
'hls': true,
|
||||
});
|
||||
}
|
||||
|
||||
$np.station.mounts.forEach(function (mount) {
|
||||
all_streams.push({
|
||||
'name': mount.name,
|
||||
'url': mount.url,
|
||||
'hls': false,
|
||||
});
|
||||
});
|
||||
$np.station.remotes.forEach(function (remote) {
|
||||
all_streams.push({
|
||||
'name': remote.name,
|
||||
'url': remote.url,
|
||||
'hls': false,
|
||||
});
|
||||
});
|
||||
|
||||
return all_streams;
|
||||
});
|
||||
|
||||
const time_total = computed(() => {
|
||||
let $np = np.value;
|
||||
return $np?.now_playing?.duration ?? 0;
|
||||
});
|
||||
|
||||
const time_percent = computed(() => {
|
||||
let $np_elapsed = np_elapsed.value;
|
||||
let $time_total = time_total.value;
|
||||
|
||||
if (!$time_total) {
|
||||
return 0;
|
||||
}
|
||||
if ($np_elapsed > $time_total) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return ($np_elapsed / $time_total) * 100;
|
||||
});
|
||||
|
||||
const time_display_played = computed(() => {
|
||||
let $np_elapsed = np_elapsed.value;
|
||||
let $time_total = time_total.value;
|
||||
|
||||
if (!$time_total) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($np_elapsed > $time_total) {
|
||||
$np_elapsed = $time_total;
|
||||
}
|
||||
|
||||
return formatTime($np_elapsed);
|
||||
});
|
||||
|
||||
const time_display_total = computed(() => {
|
||||
let $time_total = time_total.value;
|
||||
return ($time_total) ? formatTime($time_total) : null;
|
||||
});
|
||||
|
||||
const isMounted = useMounted();
|
||||
const player = ref(); // Template ref
|
||||
|
||||
const volume = useStorage('player_volume', 55);
|
||||
const isMuted = useStorage('player_is_muted', false);
|
||||
|
||||
const toggleMute = () => {
|
||||
isMuted.value = !isMuted.value;
|
||||
}
|
||||
|
||||
const fullVolume = () => {
|
||||
volume.value = 100;
|
||||
};
|
||||
|
||||
const switchStream = (new_stream) => {
|
||||
current_stream.value = new_stream;
|
||||
player.value.toggle(new_stream.url, true, new_stream.hls);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useIntervalFn(
|
||||
() => {
|
||||
let $np = np.value;
|
||||
|
||||
let current_time = Math.floor(Date.now() / 1000);
|
||||
let $np_elapsed = current_time - $np.now_playing.played_at;
|
||||
|
||||
if ($np_elapsed < 0) {
|
||||
$np_elapsed = 0;
|
||||
} else if ($np_elapsed >= $np.now_playing.duration) {
|
||||
$np_elapsed = $np.now_playing.duration;
|
||||
}
|
||||
|
||||
np_elapsed.value = $np_elapsed;
|
||||
},
|
||||
1000
|
||||
);
|
||||
|
||||
if (props.autoplay) {
|
||||
switchStream(current_stream.value);
|
||||
}
|
||||
});
|
||||
|
||||
const onNowPlayingUpdated = (np_new) => {
|
||||
emit('np_updated', np_new);
|
||||
|
||||
// Set a "default" current stream if none exists.
|
||||
let $streams = streams.value;
|
||||
let $current_stream = current_stream.value;
|
||||
|
||||
if ($current_stream.url === '' && $streams.length > 0) {
|
||||
if (props.hlsIsDefault && enable_hls.value) {
|
||||
current_stream.value = $streams[0];
|
||||
} else {
|
||||
$current_stream = null;
|
||||
|
||||
if (np_new.station.listen_url !== '') {
|
||||
$streams.forEach(function (stream) {
|
||||
if (stream.url === np_new.station.listen_url) {
|
||||
$current_stream = stream;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($current_stream === null) {
|
||||
$current_stream = $streams[0];
|
||||
}
|
||||
|
||||
current_stream.value = $current_stream;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(np, onNowPlayingUpdated, {immediate: true});
|
||||
</script>
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {get, set, templateRef, useClipboard} from "@vueuse/core";
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
|
||||
const logs = ref('Loading...');
|
||||
const $modal = templateRef('modal');
|
||||
const modal = ref(); // Template Ref
|
||||
|
||||
const show = (newLogs) => {
|
||||
let logDisplay = [];
|
||||
|
@ -26,18 +26,18 @@ const show = (newLogs) => {
|
|||
logDisplay.push(log.formatted);
|
||||
});
|
||||
|
||||
set(logs, logDisplay.join(''));
|
||||
get($modal).show();
|
||||
logs.value = logDisplay.join('');
|
||||
modal.value.show();
|
||||
};
|
||||
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const doCopy = () => {
|
||||
clipboard.copy(get(logs));
|
||||
clipboard.copy(logs.value);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
get($modal).hide();
|
||||
modal.value.hide();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div id="leaflet-container" ref="map">
|
||||
<div id="leaflet-container" ref="container">
|
||||
<slot v-if="$map" :map="$map"/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -14,16 +14,15 @@
|
|||
</style>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, provide, ref} from "vue";
|
||||
import {onMounted, provide, ref, shallowRef} from "vue";
|
||||
import L from "leaflet";
|
||||
import {get, set, templateRef} from "@vueuse/core";
|
||||
|
||||
const props = defineProps({
|
||||
attribution: String
|
||||
});
|
||||
|
||||
const $container = templateRef('map');
|
||||
const $map = ref();
|
||||
const container = ref(); // Template Ref
|
||||
const $map = shallowRef();
|
||||
|
||||
provide('map', $map);
|
||||
|
||||
|
@ -38,9 +37,10 @@ onMounted(() => {
|
|||
});
|
||||
|
||||
// Init map
|
||||
const map = L.map(get($container));
|
||||
const map = L.map(container.value);
|
||||
map.setView([40, 0], 1);
|
||||
set($map, map);
|
||||
|
||||
$map.value = map;
|
||||
|
||||
// Add tile layer
|
||||
const tileUrl = 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/{theme}_all/{z}/{x}/{y}.png';
|
||||
|
|
|
@ -1,37 +1,35 @@
|
|||
<template>
|
||||
<div ref="popup-content">
|
||||
<div ref="content">
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {get, set, templateRef} from '@vueuse/core';
|
||||
import {inject, onUnmounted, ref, toRaw, watch} from 'vue';
|
||||
import {inject, onUnmounted, ref, watch} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
position: Array
|
||||
});
|
||||
|
||||
const $map = inject('map');
|
||||
const $marker = ref();
|
||||
const map = $map.value;
|
||||
|
||||
const map = toRaw(get($map));
|
||||
const marker = L.marker(props.position);
|
||||
marker.addTo(map);
|
||||
set($marker, marker);
|
||||
|
||||
const popup = new L.Popup();
|
||||
const $popupContent = templateRef('popup-content');
|
||||
const content = ref(); // Template Ref
|
||||
|
||||
watch(
|
||||
$popupContent,
|
||||
(content) => {
|
||||
popup.setContent(content);
|
||||
content,
|
||||
(newContent) => {
|
||||
popup.setContent(newContent);
|
||||
marker.bindPopup(popup);
|
||||
},
|
||||
{immediate: true}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
get($marker).remove();
|
||||
marker.remove();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -119,60 +119,59 @@
|
|||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {DateTime} from "luxon";
|
||||
<script setup>
|
||||
import Icon from "~/components/Common/Icon";
|
||||
import IsMounted from "~/components/Common/IsMounted";
|
||||
import {get, set, useMounted} from "@vueuse/core";
|
||||
import {onMounted, ref, shallowRef, toRef, watch} from "vue";
|
||||
import {DateTime} from "luxon";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
|
||||
export default {
|
||||
name: 'BestAndWorstTab',
|
||||
components: {Icon},
|
||||
mixins: [IsMounted],
|
||||
props: {
|
||||
dateRange: Object,
|
||||
apiUrl: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
bestAndWorst: {
|
||||
best: [],
|
||||
worst: []
|
||||
},
|
||||
mostPlayed: [],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dateRange() {
|
||||
if (this.isMounted) {
|
||||
this.relist();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.relist();
|
||||
},
|
||||
methods: {
|
||||
relist() {
|
||||
this.loading = true;
|
||||
this.axios.get(this.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(this.dateRange.startDate).toISO(),
|
||||
end: DateTime.fromJSDate(this.dateRange.endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
this.bestAndWorst = response.data.bestAndWorst;
|
||||
this.mostPlayed = response.data.mostPlayed;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
getSongText(song) {
|
||||
if (song.title !== '') {
|
||||
return '<b>' + song.title + '</b><br>' + song.artist;
|
||||
}
|
||||
const props = defineProps({
|
||||
dateRange: Object,
|
||||
apiUrl: String,
|
||||
});
|
||||
|
||||
return song.text;
|
||||
const loading = ref(true);
|
||||
const bestAndWorst = shallowRef({
|
||||
best: [],
|
||||
worst: []
|
||||
});
|
||||
const mostPlayed = ref([]);
|
||||
|
||||
const dateRange = toRef(props, 'dateRange');
|
||||
const {axios} = useAxios();
|
||||
|
||||
const relist = () => {
|
||||
set(loading, true);
|
||||
axios.get(props.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(get(dateRange).startDate).toISO(),
|
||||
end: DateTime.fromJSDate(get(dateRange).endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
set(bestAndWorst, response.data.bestAndWorst);
|
||||
set(mostPlayed, response.data.mostPlayed);
|
||||
set(loading, false);
|
||||
});
|
||||
};
|
||||
|
||||
const isMounted = useMounted();
|
||||
|
||||
watch(dateRange, () => {
|
||||
if (get(isMounted)) {
|
||||
relist();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
relist();
|
||||
});
|
||||
|
||||
const getSongText = (song) => {
|
||||
if (song.title !== '') {
|
||||
return '<b>' + song.title + '</b><br>' + song.artist;
|
||||
}
|
||||
|
||||
return song.text;
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -12,9 +12,9 @@
|
|||
<slot name="by_listeners_legend"></slot>
|
||||
</legend>
|
||||
|
||||
<pie-chart style="width: 100%;" :data="top_listeners.datasets"
|
||||
:labels="top_listeners.labels">
|
||||
<span v-html="top_listeners.alt"></span>
|
||||
<pie-chart style="width: 100%;" :data="stats.top_listeners.datasets"
|
||||
:labels="stats.top_listeners.labels">
|
||||
<span v-html="stats.top_listeners.alt"></span>
|
||||
</pie-chart>
|
||||
</fieldset>
|
||||
</b-col>
|
||||
|
@ -24,9 +24,9 @@
|
|||
<slot name="by_connected_time_legend"></slot>
|
||||
</legend>
|
||||
|
||||
<pie-chart style="width: 100%;" :data="top_connected_time.datasets"
|
||||
:labels="top_connected_time.labels">
|
||||
<span v-html="top_connected_time.alt"></span>
|
||||
<pie-chart style="width: 100%;" :data="stats.top_connected_time.datasets"
|
||||
:labels="stats.top_connected_time.labels">
|
||||
<span v-html="stats.top_connected_time.alt"></span>
|
||||
</pie-chart>
|
||||
</fieldset>
|
||||
</b-col>
|
||||
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
|
||||
<data-table ref="datatable" :id="fieldKey+'_table'" paginated handle-client-side
|
||||
:fields="fields" :responsive="false" :items="all">
|
||||
:fields="fields" :responsive="false" :items="stats.all">
|
||||
<template #cell(connected_seconds_calc)="row">
|
||||
{{ formatTime(row.item.connected_seconds) }}
|
||||
</template>
|
||||
|
@ -43,74 +43,76 @@
|
|||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {DateTime} from "luxon";
|
||||
<script setup>
|
||||
import PieChart from "~/components/Common/PieChart";
|
||||
import formatTime from "~/functions/formatTime";
|
||||
import DataTable from "~/components/Common/DataTable";
|
||||
import IsMounted from "~/components/Common/IsMounted";
|
||||
import formatTime from "~/functions/formatTime";
|
||||
import {onMounted, ref, shallowRef, toRef, watch} from "vue";
|
||||
import gettext from "~/vendor/gettext";
|
||||
import {DateTime} from "luxon";
|
||||
import {get, set, useMounted} from "@vueuse/core";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
|
||||
export default {
|
||||
name: 'CommonMetricsView',
|
||||
components: {DataTable, PieChart},
|
||||
mixins: [IsMounted],
|
||||
props: {
|
||||
dateRange: Object,
|
||||
apiUrl: String,
|
||||
fieldKey: String,
|
||||
fieldLabel: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
all: [],
|
||||
top_listeners: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
alt: ''
|
||||
},
|
||||
top_connected_time: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
alt: ''
|
||||
},
|
||||
fields: [
|
||||
{key: this.fieldKey, label: this.fieldLabel, sortable: true},
|
||||
{key: 'listeners', label: this.$gettext('Listeners'), sortable: true},
|
||||
{key: 'connected_seconds_calc', label: this.$gettext('Time'), sortable: false},
|
||||
{key: 'connected_seconds', label: this.$gettext('Time (sec)'), sortable: true}
|
||||
]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dateRange() {
|
||||
if (this.isMounted) {
|
||||
this.relist();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.relist();
|
||||
},
|
||||
methods: {
|
||||
relist() {
|
||||
this.loading = true;
|
||||
this.axios.get(this.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(this.dateRange.startDate).toISO(),
|
||||
end: DateTime.fromJSDate(this.dateRange.endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
this.all = response.data.all;
|
||||
this.top_listeners = response.data.top_listeners;
|
||||
this.top_connected_time = response.data.top_connected_time;
|
||||
const props = defineProps({
|
||||
dateRange: Object,
|
||||
apiUrl: String,
|
||||
fieldKey: String,
|
||||
fieldLabel: String,
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
formatTime(time) {
|
||||
return formatTime(time);
|
||||
const loading = ref(true);
|
||||
const stats = shallowRef({
|
||||
all: [],
|
||||
top_listeners: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
alt: ''
|
||||
},
|
||||
top_connected_time: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
alt: ''
|
||||
},
|
||||
});
|
||||
|
||||
const {$gettext} = gettext;
|
||||
|
||||
const fields = shallowRef([
|
||||
{key: props.fieldKey, label: props.fieldLabel, sortable: true},
|
||||
{key: 'listeners', label: $gettext('Listeners'), sortable: true},
|
||||
{key: 'connected_seconds_calc', label: $gettext('Time'), sortable: false},
|
||||
{key: 'connected_seconds', label: $gettext('Time (sec)'), sortable: true}
|
||||
]);
|
||||
|
||||
const dateRange = toRef(props, 'dateRange');
|
||||
const {axios} = useAxios();
|
||||
|
||||
const relist = () => {
|
||||
set(loading, true);
|
||||
axios.get(props.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(get(dateRange).startDate).toISO(),
|
||||
end: DateTime.fromJSDate(get(dateRange).endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
set(stats, {
|
||||
all: response.data.all,
|
||||
top_listeners: response.data.top_listeners,
|
||||
top_connected_time: response.data.top_connected_time
|
||||
});
|
||||
set(loading, false);
|
||||
});
|
||||
};
|
||||
|
||||
const isMounted = useMounted();
|
||||
|
||||
watch(dateRange, () => {
|
||||
if (get(isMounted)) {
|
||||
relist();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
relist();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -5,11 +5,10 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {get, templateRef, watchOnce} from "@vueuse/core";
|
||||
import {Tableau20} from "~/vendor/chartjs-colorschemes/colorschemes.tableau";
|
||||
import {Chart} from "chart.js";
|
||||
import gettext from "~/vendor/gettext";
|
||||
import {onUnmounted} from "vue";
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
options: Object,
|
||||
|
@ -18,10 +17,10 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
let $chart = null;
|
||||
const $canvas = templateRef('canvas');
|
||||
const canvas = ref(); // Template Ref
|
||||
const {$gettext} = gettext;
|
||||
|
||||
watchOnce($canvas, () => {
|
||||
onMounted(() => {
|
||||
const defaultOptions = {
|
||||
type: 'bar',
|
||||
data: {
|
||||
|
@ -60,7 +59,7 @@ watchOnce($canvas, () => {
|
|||
}
|
||||
|
||||
let chartOptions = _.defaultsDeep({}, props.options, defaultOptions);
|
||||
$chart = new Chart(get($canvas).getContext('2d'), chartOptions);
|
||||
$chart = new Chart(canvas.value.getContext('2d'), chartOptions);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -45,62 +45,61 @@
|
|||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import TimeSeriesChart from "~/components/Common/TimeSeriesChart";
|
||||
import HourChart from "~/components/Stations/Reports/Overview/HourChart";
|
||||
import {DateTime} from "luxon";
|
||||
import PieChart from "~/components/Common/PieChart";
|
||||
import IsMounted from "~/components/Common/IsMounted";
|
||||
import {onMounted, ref, shallowRef, toRef, watch} from "vue";
|
||||
import {get, set, useMounted} from "@vueuse/core";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
|
||||
export default {
|
||||
name: 'ListenersByTimePeriodTab',
|
||||
components: {PieChart, HourChart, TimeSeriesChart},
|
||||
mixins: [IsMounted],
|
||||
props: {
|
||||
dateRange: Object,
|
||||
apiUrl: String,
|
||||
const props = defineProps({
|
||||
dateRange: Object,
|
||||
apiUrl: String,
|
||||
});
|
||||
|
||||
const loading = ref(true);
|
||||
|
||||
const chartData = shallowRef({
|
||||
daily: {},
|
||||
day_of_week: {
|
||||
labels: [],
|
||||
metrics: [],
|
||||
alt: ''
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
chartData: {
|
||||
daily: {},
|
||||
day_of_week: {
|
||||
labels: [],
|
||||
metrics: [],
|
||||
alt: ''
|
||||
},
|
||||
hourly: {
|
||||
labels: [],
|
||||
metrics: [],
|
||||
alt: ''
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dateRange() {
|
||||
if (this.isMounted) {
|
||||
this.relist();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.relist();
|
||||
},
|
||||
methods: {
|
||||
relist() {
|
||||
this.loading = true;
|
||||
this.axios.get(this.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(this.dateRange.startDate).toISO(),
|
||||
end: DateTime.fromJSDate(this.dateRange.endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
this.chartData = response.data;
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
hourly: {
|
||||
labels: [],
|
||||
metrics: [],
|
||||
alt: ''
|
||||
}
|
||||
});
|
||||
|
||||
const dateRange = toRef(props, 'dateRange');
|
||||
const {axios} = useAxios();
|
||||
|
||||
const relist = () => {
|
||||
set(loading, true);
|
||||
axios.get(props.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(get(dateRange).startDate).toISO(),
|
||||
end: DateTime.fromJSDate(get(dateRange).endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
set(chartData, response.data);
|
||||
set(loading, false);
|
||||
});
|
||||
}
|
||||
|
||||
const isMounted = useMounted();
|
||||
|
||||
watch(dateRange, () => {
|
||||
if (get(isMounted)) {
|
||||
relist();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
relist();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -10,74 +10,82 @@
|
|||
{{ $gettext('Listeners by Listening Time') }}
|
||||
</legend>
|
||||
|
||||
<pie-chart style="width: 100%;" :data="chart.datasets"
|
||||
:labels="chart.labels" :aspect-ratio="4">
|
||||
<span v-html="chart.alt"></span>
|
||||
<pie-chart style="width: 100%;" :data="stats.chart.datasets"
|
||||
:labels="stats.chart.labels" :aspect-ratio="4">
|
||||
<span v-html="stats.chart.alt"></span>
|
||||
</pie-chart>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<data-table ref="datatable" id="listening_time_table" paginated handle-client-side
|
||||
:fields="fields" :responsive="false" :items="all">
|
||||
:fields="fields" :responsive="false" :items="stats.all">
|
||||
</data-table>
|
||||
</div>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {DateTime} from "luxon";
|
||||
<script setup>
|
||||
import PieChart from "~/components/Common/PieChart";
|
||||
import DataTable from "~/components/Common/DataTable";
|
||||
import IsMounted from "~/components/Common/IsMounted";
|
||||
import {onMounted, ref, shallowRef, toRef, watch} from "vue";
|
||||
import gettext from "~/vendor/gettext";
|
||||
import {DateTime} from "luxon";
|
||||
import {get, set, useMounted} from "@vueuse/core";
|
||||
import {useAxios} from "~/vendor/axios";
|
||||
|
||||
export default {
|
||||
name: 'ListeningTimeTab',
|
||||
components: {DataTable, PieChart},
|
||||
mixins: [IsMounted],
|
||||
props: {
|
||||
dateRange: Object,
|
||||
apiUrl: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
all: [],
|
||||
chart: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
alt: ''
|
||||
},
|
||||
fields: [
|
||||
{key: 'label', label: this.$gettext('Listening Time'), sortable: false},
|
||||
{key: 'value', label: this.$gettext('Listeners'), sortable: false}
|
||||
]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
dateRange() {
|
||||
if (this.isMounted) {
|
||||
this.relist();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.relist();
|
||||
},
|
||||
methods: {
|
||||
relist() {
|
||||
this.loading = true;
|
||||
this.axios.get(this.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(this.dateRange.startDate).toISO(),
|
||||
end: DateTime.fromJSDate(this.dateRange.endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
this.all = response.data.all;
|
||||
this.chart = response.data.chart;
|
||||
const props = defineProps({
|
||||
dateRange: Object,
|
||||
apiUrl: String
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
const loading = ref(true);
|
||||
const stats = shallowRef({
|
||||
all: [],
|
||||
chart: {
|
||||
labels: [],
|
||||
datasets: [],
|
||||
alt: ''
|
||||
}
|
||||
});
|
||||
|
||||
const {$gettext} = gettext;
|
||||
const fields = shallowRef([
|
||||
{key: 'label', label: $gettext('Listening Time'), sortable: false},
|
||||
{key: 'value', label: $gettext('Listeners'), sortable: false}
|
||||
]);
|
||||
|
||||
const dateRange = toRef(props, 'dateRange');
|
||||
const {axios} = useAxios();
|
||||
|
||||
const relist = () => {
|
||||
set(loading, true);
|
||||
|
||||
axios.get(props.apiUrl, {
|
||||
params: {
|
||||
start: DateTime.fromJSDate(get(dateRange).startDate).toISO(),
|
||||
end: DateTime.fromJSDate(get(dateRange).endDate).toISO()
|
||||
}
|
||||
}).then((response) => {
|
||||
set(
|
||||
stats,
|
||||
{
|
||||
all: response.data.all,
|
||||
chart: response.data.chart
|
||||
}
|
||||
);
|
||||
set(loading, false);
|
||||
});
|
||||
}
|
||||
|
||||
const isMounted = useMounted();
|
||||
|
||||
watch(dateRange, () => {
|
||||
if (get(isMounted)) {
|
||||
relist();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
relist();
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue