Fix template refs, rebuild NP components.

This commit is contained in:
Buster Neece 2022-12-17 18:01:37 -06:00
parent 6cb6236fa7
commit 8259989f6a
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
23 changed files with 709 additions and 780 deletions

View File

@ -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({

View File

@ -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({

View File

@ -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({

View File

@ -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;
},

View File

@ -1,13 +0,0 @@
<script>
export default {
name: 'IsMounted',
data() {
return {
isMounted: false
}
},
mounted() {
this.isMounted = true;
}
}
</script>

View File

@ -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;
}

View File

@ -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>

View File

@ -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(() => {

View File

@ -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')

View File

@ -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({

View File

@ -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(() => {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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({

View File

@ -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';

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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(() => {

View File

@ -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>

View File

@ -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>