Merge commit '79df3bb8b426bd747e6b7bfc621cda06dfef7e5f'

This commit is contained in:
Buster Neece 2022-12-12 12:47:04 -06:00
commit 4b72fbe3b7
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
116 changed files with 2481 additions and 1180 deletions

View File

@ -1,5 +1,5 @@
{
"vueCompilerOptions": {
"target": 2.7
"target": 3.2
}
}

File diff suppressed because it is too large Load Diff

View File

@ -16,9 +16,11 @@
"@fullcalendar/daygrid": "^5.9.0",
"@fullcalendar/luxon2": "^5.10.2",
"@fullcalendar/timegrid": "^5.9.0",
"@fullcalendar/vue": "^5.9.0",
"@fullcalendar/vue3": "^5.11",
"@vue/compat": "^3.2.45",
"@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0",
"@vueuse/core": "^9.6.0",
"axios": "^1",
"bootstrap": "^4.6.0 <5",
"bootstrap-notify": "^3.1.3",
@ -61,14 +63,13 @@
"sass-loader": "^13",
"store": "^2",
"sweetalert2": "11.4.8",
"vue": "^2 <3",
"vue-axios": "^2 <3",
"vue-clipboard2": "^0.3.3",
"vue-gettext": "^2.1.12",
"vue-loader": "^15 <16",
"vue2-daterange-picker": "^0.6.6",
"vue2-leaflet": "^2.7.1",
"vue2-leaflet-fullscreen": "^1.0.1",
"vue": "^3.2",
"vue-axios": "^3.5",
"vue-loader": "^17",
"vue-style-loader": "^4.1.3",
"vue3-clipboard": "^1.0.0",
"vue3-daterange-picker": "^1",
"vue3-gettext": "^2.3.4",
"vuedraggable": "^2.24.1",
"wavesurfer.js": "^6",
"webpack": "^5.52.1",

View File

@ -95,6 +95,7 @@ $enable-print-styles: true !default;
// Overrides for the Daemonite Material theme
@import 'azuracast/overrides/body';
@import 'azuracast/overrides/buttons';
@import 'azuracast/overrides/card';
@import 'azuracast/overrides/footer';
@import 'azuracast/overrides/forms';

View File

@ -0,0 +1,21 @@
.btn > .material-icons {
margin-right: calc($btn-padding-x / 2);
}
.btn-lg > .material-icons {
margin-right: calc($btn-padding-x-lg / 2);
}
.btn-sm > .material-icons {
margin-right: calc($btn-padding-x-sm / 2);
}
.buttons {
& > * {
margin-right: .5rem;
}
& > *:last-child {
margin-right: 0;
}
}

View File

@ -3,4 +3,16 @@
@include reset-material-icons($material-icon-size * 2);
line-height: 0.5em;
}
.btn > & {
margin-right: calc($btn-padding-x / 2);
}
.btn-lg > & {
margin-right: calc($btn-padding-x-lg / 2);
}
.btn-sm > & {
margin-right: calc($btn-padding-x-sm / 2);
}
}

View File

@ -0,0 +1,469 @@
$ranges-hover-bg-color: #eee !default;
$ranges-hover-text-color: #000 !default;
$ranges-active-bg-color: #08c !default;
$ranges-active-text-color: #fff !default;
//Apply/OK buttons
$primary-button-bg: #28a745 !default;
$primary-button-color: #fff !default;
//Cancel button
$secondary-button-bg: #6c757d !default;
$secondary-button-color: #fff !default;
$btn-border-width: 1px;
.daterangepicker {
.ranges {
text-align: left;
margin: 0;
width: 100%;
}
.ranges ul {
list-style: none;
margin: 0 auto;
padding: 0;
width: 100%;
}
.ranges li {
font-size: 12px;
padding: 8px 12px;
cursor: pointer;
}
.ranges li:hover {
background-color: $ranges-hover-bg-color;
color: $ranges-hover-text-color;
}
.ranges li.active {
background-color: $ranges-active-bg-color;
color: $ranges-active-text-color;
}
.monthselect, .yearselect {
font-size: 12px;
padding: 1px;
height: auto;
margin: 0;
cursor: default;
width: calc(50% - 1rem);
}
.monthselect {
margin-right: 1rem;
}
.calendar-time {
text-align: center;
margin: 4px auto 0 auto;
line-height: 30px;
position: relative;
display: flex;
justify-content: center;
}
.calendar-time select.disabled {
color: #ccc;
cursor: not-allowed;
}
select.hourselect, select.minuteselect, select.secondselect, select.ampmselect {
width: 50px;
margin: 2px;
background: #eee;
border: 1px solid #eee;
padding: 2px;
outline: 0;
font-size: 12px;
}
@import "variables";
.drp-buttons .btn {
margin-left: 8px;
font-size: 12px;
font-weight: bold;
padding: 4px 8px;
}
.btn {
display: inline-block;
//font-family: $btn-font-family;
//font-weight: $btn-font-weight;
//color: $body-color;
text-align: center;
vertical-align: middle;
user-select: none;
background-color: transparent;
border: $btn-border-width solid transparent;
}
.btn-success, .btn-primary {
background-color: $primary-button-bg;
color: $primary-button-color;
}
.btn-secondary {
background-color: $secondary-button-bg;
color: $secondary-button-color;
}
}
.vue-daterange-picker {
*, ::after, ::before {
box-sizing: border-box;
}
}
.drp-calendar .col .left {
flex: 0 0 auto;
}
.daterangepicker {
&.hide-calendars.show-ranges {
.ranges {
width: 100%;
ul {
width: 100%;
}
}
}
.calendars-container {
display: flex;
}
}
.daterangepicker[readonly] {
pointer-events: none;
}
//imported
.daterangepicker {
position: absolute;
color: inherit;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ddd;
width: 278px;
max-width: none;
padding: 0;
margin-top: 7px;
top: 100px;
left: 20px;
z-index: 3001;
display: none;
font-size: 15px;
line-height: 1em;
}
.daterangepicker:before, .daterangepicker:after {
position: absolute;
display: inline-block;
border-bottom-color: rgba(0, 0, 0, 0.2);
content: '';
}
.daterangepicker:before {
top: -7px;
border-right: 7px solid transparent;
border-left: 7px solid transparent;
border-bottom: 7px solid #ccc;
}
.daterangepicker:after {
top: -6px;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-left: 6px solid transparent;
}
.daterangepicker.opensleft:before {
right: 9px;
}
.daterangepicker.opensleft:after {
right: 10px;
}
.daterangepicker.openscenter:before {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto;
}
.daterangepicker.openscenter:after {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto;
}
.daterangepicker.opensright:before {
left: 9px;
}
.daterangepicker.opensright:after {
left: 10px;
}
.daterangepicker.drop-up {
margin-top: -7px;
}
.daterangepicker.drop-up:before {
top: initial;
bottom: -7px;
border-bottom: initial;
border-top: 7px solid #ccc;
}
.daterangepicker.drop-up:after {
top: initial;
bottom: -6px;
border-bottom: initial;
border-top: 6px solid #fff;
}
.daterangepicker.single .drp-selected {
display: none;
}
.daterangepicker.show-calendar .drp-calendar {
display: block;
}
.daterangepicker.show-calendar .drp-buttons {
display: block;
}
.daterangepicker.auto-apply .drp-buttons {
display: none;
}
.daterangepicker .drp-calendar {
display: none;
max-width: 270px;
width: 270px;
}
.daterangepicker .drp-calendar.left {
padding: 8px 0 8px 8px;
}
.daterangepicker .drp-calendar.right {
padding: 8px;
}
.daterangepicker .drp-calendar.single .calendar-table {
border: none;
}
.daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span {
color: #fff;
border: solid black;
border-width: 0 2px 2px 0;
border-radius: 0;
display: inline-block;
padding: 3px;
}
.daterangepicker .calendar-table .next span {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}
.daterangepicker .calendar-table .prev span {
transform: rotate(135deg);
-webkit-transform: rotate(135deg);
}
.daterangepicker .calendar-table th, .daterangepicker .calendar-table td {
white-space: nowrap;
text-align: center;
vertical-align: middle;
min-width: 32px;
width: 32px;
height: 24px;
line-height: 24px;
font-size: 12px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
}
.daterangepicker .calendar-table {
border: 1px solid #fff;
border-radius: 4px;
background-color: #fff;
}
.daterangepicker .calendar-table table {
width: 100%;
margin: 0;
border-spacing: 0;
border-collapse: collapse;
display: table;
}
.daterangepicker td.available:hover, .daterangepicker th.available:hover {
background-color: #eee;
border-color: transparent;
color: inherit;
}
.daterangepicker td.week, .daterangepicker th.week {
font-size: 80%;
color: #ccc;
}
.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date {
background-color: #fff;
border-color: transparent;
color: #999;
}
.daterangepicker td.in-range {
background-color: #ebf4f8;
border-color: transparent;
color: #000;
border-radius: 0;
}
.daterangepicker td.start-date {
border-radius: 4px 0 0 4px;
}
.daterangepicker td.end-date {
border-radius: 0 4px 4px 0;
}
.daterangepicker td.start-date.end-date {
border-radius: 4px;
}
.daterangepicker td.active, .daterangepicker td.active:hover {
background-color: #357ebd;
border-color: transparent;
color: #fff;
}
.daterangepicker th.month {
width: auto;
}
.daterangepicker td.disabled, .daterangepicker option.disabled {
color: #999;
cursor: not-allowed;
text-decoration: line-through;
}
.daterangepicker select.yearselect {
width: 40%;
}
.daterangepicker .drp-buttons {
clear: both;
text-align: right;
padding: 8px;
border-top: 1px solid #ddd;
display: none;
line-height: 12px;
vertical-align: middle;
}
.daterangepicker .drp-selected {
display: inline-block;
font-size: 12px;
padding-right: 8px;
}
.daterangepicker.show-ranges .drp-calendar.left {
border-left: 1px solid #ddd;
}
.daterangepicker.show-calendar .ranges {
margin-top: 8px;
width: auto;
}
/* Larger Screen Styling */
@media (min-width: 564px) {
.daterangepicker {
width: auto;
}
.daterangepicker .ranges ul {
width: 140px;
}
.daterangepicker.single .ranges ul {
width: 100%;
}
.daterangepicker.single .drp-calendar.left {
clear: none;
}
.daterangepicker.ltr {
direction: ltr;
text-align: left;
}
.daterangepicker.ltr .drp-calendar.left {
clear: left;
margin-right: 0;
}
.daterangepicker.ltr .drp-calendar.left .calendar-table {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.daterangepicker.ltr .drp-calendar.right {
margin-left: 0;
}
.daterangepicker.ltr .drp-calendar.right .calendar-table {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.daterangepicker.ltr .drp-calendar.left .calendar-table {
padding-right: 8px;
}
.daterangepicker.rtl {
direction: rtl;
text-align: right;
}
.daterangepicker.rtl .drp-calendar.left {
clear: right;
margin-left: 0;
}
.daterangepicker.rtl .drp-calendar.left .calendar-table {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.daterangepicker.rtl .drp-calendar.right {
margin-right: 0;
}
.daterangepicker.rtl .drp-calendar.right .calendar-table {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.daterangepicker.rtl .drp-calendar.left .calendar-table {
padding-left: 12px;
}
.daterangepicker.rtl .ranges, .daterangepicker.rtl .drp-calendar {
text-align: right;
}
}
@media (min-width: 730px) {
.daterangepicker .drp-calendar.left {
clear: none !important;
}
}

View File

@ -1,72 +1,89 @@
import Vue from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import GetTextPlugin from 'vue-gettext';
import translations from '../../translations/translations.json';
import pinia from './vendor/pinia';
import usePinia from './vendor/pinia';
import gettext from './vendor/gettext';
import {createApp} from "vue";
import useBootstrapVue from "./vendor/bootstrapVue";
import useSweetAlert from "./vendor/sweetalert";
import useVueClipboard from "~/vendor/clipboard";
document.addEventListener('DOMContentLoaded', function () {
// Configure localization
Vue.use(GetTextPlugin, {
defaultLanguage: 'en_US',
translations: translations,
silent: true
});
export default function (component, options) {
return function (el, props) {
const vueApp = createApp(
component,
{
...options,
...props
}
);
if (typeof App.locale !== 'undefined') {
Vue.config.language = App.locale;
}
/* Gettext */
if (typeof App.locale !== 'undefined') {
vueApp.config.language = App.locale;
}
// Configure auto-CSRF on requests
if (typeof App.api_csrf !== 'undefined') {
axios.defaults.headers.common['X-API-CSRF'] = App.api_csrf;
}
vueApp.use(gettext);
Vue.use(VueAxios, axios);
/* Axios */
Vue.prototype.$eventHub = new Vue();
});
// Configure auto-CSRF on requests
if (typeof App.api_csrf !== 'undefined') {
axios.defaults.headers.common['X-API-CSRF'] = App.api_csrf;
}
export default function (component) {
return function (el, props) {
return new Vue({
el: el,
pinia,
created() {
let handleAxiosError = (error) => {
let notifyMessage = this.$gettext('An error occurred and your request could not be completed.');
if (error.response) {
// Request made and server responded
notifyMessage = error.response.data.message;
console.error(notifyMessage);
} else if (error.request) {
// The request was made but no response was received
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error', error.message);
}
vueApp.use(VueAxios, axios);
if (typeof this.$notifyError === 'function') {
this.$notifyError(notifyMessage);
}
};
vueApp.mixin({
mounted() {
const handleAxiosError = (error) => {
const {$gettext} = gettext;
axios.interceptors.request.use((config) => {
return config;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
let notifyMessage = $gettext('An error occurred and your request could not be completed.');
if (error.response) {
// Request made and server responded
notifyMessage = error.response.data.message;
console.error(notifyMessage);
} else if (error.request) {
// The request was made but no response was received
console.error(error.request);
} else {
// Something happened in setting up the request that triggered an Error
console.error('Error', error.message);
}
if (typeof this.$notifyError === 'function') {
this.$notifyError(notifyMessage);
}
};
axios.interceptors.request.use((config) => {
return config;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
});
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
});
}
});
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
});
},
render: createElement => createElement(component, { props: props })
});
};
/* Pinia */
usePinia(vueApp);
/* Bootstrap Vue */
useBootstrapVue(vueApp);
/* SweetAlert */
useSweetAlert(vueApp);
/* Clipboard */
useVueClipboard(vueApp);
vueApp.mount(el);
};
}

View File

@ -3,7 +3,7 @@
<permissions-form-station-row
v-for="(row, index) in form.permissions.$model.station" :key="index"
:stations="stations" :station-permissions="stationPermissions"
:row.sync="row" @remove="remove(index)"
v-model:row="form.permissions.$model.station[index]" @remove="remove(index)"
></permissions-form-station-row>
<b-button-group v-if="hasRemainingStations">

View File

@ -43,7 +43,7 @@
</data-table>
</b-card>
<admin-stations-edit-modal ref="editModal" :create-url="listUrl" v-bind="$props"
<admin-stations-edit-modal v-bind="$props" ref="editModal" :create-url="listUrl"
@relist="relist"></admin-stations-edit-modal>
<admin-stations-clone-modal ref="cloneModal" @relist="relist"></admin-stations-clone-modal>

View File

@ -1,7 +1,7 @@
<template>
<b-modal size="lg" id="station_edit_modal" ref="modal" :title="langTitle" :busy="loading"
@shown="resetForm" @hidden="clearContents">
<admin-stations-form ref="form" v-bind="$props" is-modal :create-url="createUrl" :edit-url="editUrl"
<admin-stations-form v-bind="$props" ref="form" is-modal :create-url="createUrl" :edit-url="editUrl"
:is-edit-mode="isEditMode" @error="close" @submitted="onSubmit"
@validUpdate="onValidUpdate" @loadingUpdate="onLoadingUpdate">
<template #submitButton>

View File

@ -1,18 +1,29 @@
<template>
<a :href="src" class="album-art" target="_blank" data-fancybox="gallery">
<b-img class="album_art" :src="src" loading="lazy"
:style="{ width: this.width+'px', height: 'auto', 'border-radius': '5px' }"></b-img>
<a v-if="src" :href="src" class="album-art" target="_blank" data-fancybox="gallery">
<img class="album_art" :src="src" loading="lazy" alt="">
</a>
</template>
<script>
export default {
props: {
src: String,
width: {
type: Number,
default: 40
}
<style scoped>
img.album_art {
width: v-bind(widthPx);
height: auto;
border-radius: 5px;
}
</style>
<script setup>
import {computed} from "vue";
const props = defineProps({
src: String,
width: {
type: Number,
default: 40
}
};
});
const widthPx = computed(() => {
return props.width + 'px';
});
</script>

View File

@ -5,29 +5,22 @@
</button>
</template>
<script>
import '~/vendor/clipboard.js';
import Icon from './Icon';
<script setup>
import Icon from "~/components/Common/Icon.vue";
import {copyToClipboard} from "~/vendor/clipboard";
export default {
components: { Icon },
props: {
text: {
type: String,
required: true,
},
hideText: {
type: Boolean,
default: false
}
const props = defineProps({
text: {
type: String,
required: true,
},
methods: {
doCopy() {
this.$copyText(this.text).then(function (e) {
}, function (e) {
console.error(e);
})
}
hideText: {
type: Boolean,
default: false
}
});
const doCopy = () => {
copyToClipboard(props.text);
};
</script>

View File

@ -60,14 +60,14 @@
</div>
<div class="datatable-main">
<b-table ref="table" show-empty striped hover :selectable="selectable" :api-url="apiUrl" :per-page="perPage"
:current-page.sync="currentPage" @row-selected="onRowSelected" :items="itemProvider"
v-model:current-page="currentPage" @row-selected="onRowSelected" :items="itemProvider"
:fields="visibleFields"
:empty-text="langNoRecords" :empty-filtered-text="langNoRecords" :responsive="responsive"
:no-provider-paging="handleClientSide" :no-provider-sorting="handleClientSide"
:no-provider-filtering="handleClientSide"
tbody-tr-class="align-middle" thead-tr-class="align-middle" selected-variant=""
:filter="filter" @filtered="onFiltered" @refreshed="onRefreshed"
:sort-by.sync="sortBy" :sort-desc.sync="sortDesc" @sort-changed="onSortChanged">
v-model:sort-by="sortBy" v-model:sort-desc="sortDesc" @sort-changed="onSortChanged">
<template #head(selected)="data">
<b-form-checkbox :aria-label="langSelectAll" :checked="allSelected"
@change="toggleSelected"></b-form-checkbox>
@ -105,9 +105,9 @@
</div>
</div>
</template>
<slot v-for="(_, name) in $slots" :name="name" :slot="name"/>
<template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData"/>
<template v-for="(_, slot) of $slots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</b-table>
</div>

View File

@ -1,37 +1,38 @@
<template>
<date-range-picker
ref="picker" controlContainerClass="" opens="left" show-dropdowns
v-bind="$props"
:time-picker-increment="1" :ranges="ranges" @update="onUpdate">
v-bind="$props" ref="picker" controlContainerClass="" opens="left" show-dropdowns
:time-picker-increment="1" :ranges="ranges" v-model="dateRange" v-model:date-range="dateRange"
@select="onSelect">
<template #input="datePicker">
<a class="btn btn-bg dropdown-toggle" id="reportrange" href="#" @click.prevent="">
<icon icon="date_range"></icon>
{{ datePicker.rangeText }}
</a>
</template>
<slot v-for="(_, name) in $slots" :name="name" :slot="name"/>
<template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData"/>
<template v-for="(_, slot) of $slots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</date-range-picker>
</template>
<style lang="css">
@import '../../../node_modules/vue2-daterange-picker/dist/vue2-daterange-picker.css';
<style lang="scss">
@import 'vue3-daterange-picker/src/assets/daterangepicker';
</style>
<script>
import DateRangePicker from 'vue2-daterange-picker';
import DateRangePicker from 'vue3-daterange-picker';
import Icon from "./Icon";
import {DateTime} from 'luxon';
export default {
name: 'DateRangeDropdown',
components: {DateRangePicker, Icon},
emits: ['update', 'input'],
emits: ['update:modelValue', 'update'],
inheritAttrs: false,
model: {
prop: 'dateRange',
event: 'update',
prop: 'modelValue',
event: 'update:modelValue'
},
props: {
tz: {
@ -54,9 +55,8 @@ export default {
type: Boolean,
default: false,
},
dateRange: { // for v-model
type: [Object],
default: null,
modelValue: {
type: Object,
required: true
},
customRanges: {
@ -65,7 +65,14 @@ export default {
},
},
computed: {
dateRange: {
get() {
return this.modelValue;
},
set(newValue) {
// Noop
}
},
ranges() {
let ranges = {};
@ -113,8 +120,9 @@ export default {
}
},
methods: {
onUpdate(newValue) {
this.$emit('update', newValue);
onSelect(range) {
this.$emit('update:modelValue', range);
this.$emit('update', range);
}
}
}

View File

@ -1,22 +1,24 @@
<template>
<div class="flow-upload">
<div class="upload-progress">
<div class="uploading-file pt-1" v-for="(file, _) in files" :id="'file_upload_' + file.uniqueIdentifier"
v-if="file.is_visible" :class="{ 'text-success': file.is_completed, 'text-danger': file.error }">
<h6 class="fileuploadname m-0">{{ file.name }}</h6>
<b-progress v-if="!file.is_completed" :value="file.progress_percent" :max="100"
show-progress class="h-15 my-1"></b-progress>
<div class="upload-status" v-if="file.error">
{{ file.error }}
<template v-for="(file, _) in files">
<div v-if="file.is_visible" class="uploading-file pt-1" :id="'file_upload_' + file.uniqueIdentifier"
:class="{ 'text-success': file.is_completed, 'text-danger': file.error }">
<h6 class="fileuploadname m-0">{{ file.name }}</h6>
<b-progress v-if="!file.is_completed" :value="file.progress_percent" :max="100"
show-progress class="h-15 my-1"></b-progress>
<div class="upload-status" v-if="file.error">
{{ file.error }}
</div>
<div class="size">{{ formatFileSize(file.size) }}</div>
</div>
<div class="size">{{ formatFileSize(file.size) }}</div>
</div>
</template>
</div>
<div class="file-drop-target" ref="file_drop_target">
<translate key="lang_upload_target">Drag file(s) here to upload or</translate>
<button ref="file_browse_target" class="file-upload btn btn-primary text-center ml-1" type="button">
<translate key="lang_select_file">Select File</translate>
<icon icon="cloud_upload"></icon>
<translate key="lang_select_file">Select File</translate>
</button>
<small class="file-name"></small>
<input type="file" :accept="validMimeTypesList" :multiple="allowMultiple"

View File

@ -27,9 +27,8 @@
</slot>
</template>
<slot v-for="(_, name) in $slots" :name="name" :slot="name"/>
<template v-for="(_, name) in filteredScopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData"/>
<template v-for="(_, slot) of filteredScopedSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</b-modal>
</template>
@ -75,7 +74,7 @@ export default {
},
computed: {
filteredScopedSlots() {
return _.filter(this.$scopedSlots, (slot, name) => {
return _.filter(this.$slots, (slot, name) => {
return !_.includes([
'default', 'modal-footer'
], name);

View File

@ -87,7 +87,6 @@ export default {
}
this.$emit('np_updated', np_new);
this.$eventHub.$emit('np_updated', np_new);
document.dispatchEvent(new CustomEvent("now-playing", {
detail: np_new

View File

@ -4,60 +4,54 @@
</canvas>
</template>
<script>
import _ from 'lodash';
import {Chart} from 'chart.js';
import {Tableau20} from '~/vendor/chartjs-colorschemes/colorschemes.tableau.js';
<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";
export default {
name: 'PieChart',
inheritAttrs: true,
props: {
options: Object,
data: Array,
labels: Array,
aspectRatio: {
type: Number,
default: 2
}
},
data() {
return {
_chart: null
};
},
mounted() {
this.renderChart();
},
methods: {
renderChart() {
const defaultOptions = {
type: 'pie',
data: {
labels: this.labels,
datasets: this.data
},
options: {
aspectRatio: this.aspectRatio,
plugins: {
colorschemes: {
scheme: Tableau20
}
}
}
};
if (this._chart)
this._chart.destroy();
let chartOptions = _.defaultsDeep({}, this.options, defaultOptions);
this._chart = new Chart(this.$refs.canvas.getContext('2d'), chartOptions);
}
},
beforeDestroy() {
if (this._chart) {
this._chart.destroy();
}
const props = defineProps({
options: Object,
data: Array,
labels: Array,
aspectRatio: {
type: Number,
default: 2
}
};
});
const $canvas = templateRef('canvas');
let $chart = null;
watchOnce($canvas, () => {
const defaultOptions = {
type: 'pie',
data: {
labels: props.labels,
datasets: props.data
},
options: {
aspectRatio: props.aspectRatio,
plugins: {
colorschemes: {
scheme: Tableau20
}
}
}
};
if ($chart) {
$chart.destroy();
}
let chartOptions = _.defaultsDeep({}, props.options, defaultOptions);
$chart = new Chart(get($canvas).getContext('2d'), chartOptions);
});
onUnmounted(() => {
if ($chart) {
$chart.destroy();
}
});
</script>

View File

@ -4,7 +4,7 @@
<script>
import '@fullcalendar/core/vdom';
import FullCalendar from '@fullcalendar/vue';
import FullCalendar from '@fullcalendar/vue3';
import allLocales from '@fullcalendar/core/locales-all';
import luxon2Plugin from '@fullcalendar/luxon2';
import timeGridPlugin from '@fullcalendar/timegrid';

View File

@ -16,6 +16,7 @@
<script>
import '~/vendor/clipboard.js';
import StreamingLogView from "~/components/Common/StreamingLogView";
import {copyToClipboard} from "~/vendor/clipboard";
export default {
name: 'StreamingLogModal',
@ -36,7 +37,7 @@ export default {
this.$refs.modal.show();
},
doCopy() {
this.$copyText(this.$refs.logView.getContents());
copyToClipboard(this.$refs.logView.getContents());
},
close() {
this.$refs.modal.hide();

View File

@ -4,103 +4,100 @@
</canvas>
</template>
<script>
import _ from 'lodash';
<script setup>
import {get, templateRef, watchOnce} from "@vueuse/core";
import {Tableau20} from "~/vendor/chartjs-colorschemes/colorschemes.tableau";
import {DateTime} from "luxon";
import {Chart} from 'chart.js';
import _ from "lodash";
import {Chart} from "chart.js";
import {onUnmounted} from "vue";
import gettext from "~/vendor/gettext";
import {Tableau20} from '~/vendor/chartjs-colorschemes/colorschemes.tableau.js';
const props = defineProps({
options: Object,
data: Array
});
export default {
name: 'TimeSeriesChart',
inheritAttrs: true,
props: {
options: Object,
data: Array
},
data() {
return {
_chart: null
};
},
mounted () {
this.renderChart();
},
methods: {
renderChart () {
const defaultOptions = {
type: 'line',
data: {
datasets: this.data
const $canvas = templateRef('canvas');
let $chart = null;
const {$gettext} = gettext;
watchOnce($canvas, () => {
const defaultOptions = {
type: 'line',
data: {
datasets: props.data
},
options: {
aspectRatio: 3,
plugins: {
zoom: {
// Container for pan options
pan: {
enabled: true,
mode: 'x'
}
},
options: {
aspectRatio: 3,
plugins: {
zoom: {
// Container for pan options
pan: {
enabled: true,
mode: 'x'
}
},
colorschemes: {
scheme: Tableau20
}
colorschemes: {
scheme: Tableau20
}
},
scales: {
x: {
type: 'time',
distribution: 'linear',
display: true,
min: DateTime.now().minus({days: 30}).toJSDate(),
max: DateTime.now().toJSDate(),
time: {
unit: 'day',
tooltipFormat: DateTime.DATE_SHORT,
},
scales: {
x: {
type: 'time',
distribution: 'linear',
display: true,
min: DateTime.now().minus({days: 30}).toJSDate(),
max: DateTime.now().toJSDate(),
time: {
unit: 'day',
tooltipFormat: DateTime.DATE_SHORT,
},
ticks: {
source: 'data',
autoSkip: true
}
},
y: {
display: true,
scaleLabel: {
display: true,
labelString: this.$gettext('Listeners')
},
ticks: {
min: 0
}
}
ticks: {
source: 'data',
autoSkip: true
}
},
y: {
display: true,
scaleLabel: {
display: true,
labelString: $gettext('Listeners')
},
tooltips: {
intersect: false,
mode: 'index',
callbacks: {
label: function (tooltipItem, myData) {
let label = myData.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
label += parseFloat(tooltipItem.value).toFixed(2);
return label;
}
}
ticks: {
min: 0
}
}
};
if (this._chart) this._chart.destroy();
let chartOptions = _.defaultsDeep({}, this.options, defaultOptions);
this._chart = new Chart(this.$refs.canvas.getContext('2d'), chartOptions);
}
},
beforeDestroy () {
if (this._chart) {
this._chart.destroy();
},
tooltips: {
intersect: false,
mode: 'index',
callbacks: {
label: function (tooltipItem, myData) {
let label = myData.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
label += parseFloat(tooltipItem.value).toFixed(2);
return label;
}
}
}
}
};
if ($chart) {
$chart.destroy();
}
};
let chartOptions = _.defaultsDeep({}, props.options, defaultOptions);
$chart = new Chart(get($canvas).getContext('2d'), chartOptions);
});
onUnmounted(() => {
if ($chart) {
$chart.destroy();
}
});
</script>

View File

@ -10,12 +10,12 @@
<h3 class="card-subtitle">{{ user.email }}</h3>
</div>
<div class="flex-md-shrink-0 mt-3 mt-md-0">
<div class="flex-md-shrink-0 mt-3 mt-md-0 buttons">
<a class="btn btn-bg" role="button" :href="profileUrl">
<icon icon="account_circle"></icon>
<translate key="dashboard_btn_my_account">My Account</translate>
</a>
<a v-if="showAdmin" class="btn btn-bg ml-2" role="button" :href="adminUrl">
<a v-if="showAdmin" class="btn btn-bg" role="button" :href="adminUrl">
<icon icon="settings"></icon>
<translate key="dashboard_btn_administration">Administration</translate>
</a>
@ -144,7 +144,9 @@
</template>
</td>
<td class="text-center">
<icon class="sm align-middle" icon="headset"></icon>
<span class="pr-1">
<icon class="sm align-middle" icon="headset"></icon>
</span>
<template v-if="item.links.listeners">
<a :href="item.links.listeners">
{{ item.listeners.total }}

View File

@ -3,7 +3,7 @@ export default {
name: 'BFormFieldset',
methods: {
getSlot(name, scope = {}) {
let slot = this.$scopedSlots[name] || this.$slots[name]
let slot = this.$slots[name]
return typeof slot === 'function' ? slot(scope) : slot
}
},

View File

@ -13,9 +13,8 @@
<slot name="description" v-bind="slotProps"></slot>
</template>
<slot v-for="(_, name) in $slots" :name="name" :slot="name"/>
<template v-for="(_, name) in filteredScopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData"/>
<template v-for="(_, slot) of filteredScopedSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</b-form-group>
</template>
@ -33,7 +32,7 @@ export default {
},
computed: {
filteredScopedSlots() {
return _.filter(this.$scopedSlots, (slot, name) => {
return _.filter(this.$slots, (slot, name) => {
return !_.includes([
'default', 'label', 'description'
], name);

View File

@ -2,7 +2,7 @@
<b-form-group v-bind="$attrs" :label-for="id" :state="fieldState">
<template #default>
<slot name="default" v-bind="{ id, field, state: fieldState }">
<b-form-checkbox :id="id" :name="name" v-model="field.$model" v-bind="inputAttrs">
<b-form-checkbox v-bind="inputAttrs" v-model="field.$model" :id="id" :name="name">
<slot name="label" :lang="'lang_'+id">
</slot>
@ -25,9 +25,8 @@
<slot name="description" v-bind="slotProps" :lang="'lang_'+id+'_desc'"></slot>
</template>
<slot v-for="(_, name) in $slots" :name="name" :slot="name"/>
<template v-for="(_, name) in filteredScopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData"/>
<template v-for="(_, slot) of filteredScopedSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</b-form-group>
</template>
@ -64,7 +63,7 @@ export default {
},
computed: {
filteredScopedSlots() {
return _.filter(this.$scopedSlots, (slot, name) => {
return _.filter(this.$slots, (slot, name) => {
return !_.includes([
'default', 'description'
], name);

View File

@ -2,13 +2,12 @@
<b-form-group v-bind="$attrs" :label-for="id" :state="fieldState">
<template #default>
<slot name="default" v-bind="{ id, field, state: fieldState }">
<b-form-textarea v-if="inputType === 'textarea'" ref="input" :id="id" :name="name"
v-model="modelValue"
:required="isRequired" :number="isNumeric" :trim="inputTrim" v-bind="inputAttrs"
<b-form-textarea v-bind="inputAttrs" v-if="inputType === 'textarea'" ref="input" :id="id" :name="name"
v-model="modelValue" :required="isRequired" :number="isNumeric" :trim="inputTrim"
:autofocus="autofocus" :state="fieldState"></b-form-textarea>
<b-form-input v-else ref="input" :type="inputType" :id="id" :name="name" v-model="modelValue"
:required="isRequired" :number="isNumeric" :trim="inputTrim"
:autofocus="autofocus" v-bind="inputAttrs" :state="fieldState"></b-form-input>
<b-form-input v-bind="inputAttrs" v-else ref="input" :type="inputType" :id="id" :name="name"
v-model="modelValue" :required="isRequired" :number="isNumeric" :trim="inputTrim"
:autofocus="autofocus" :state="fieldState"></b-form-input>
</slot>
<b-form-invalid-feedback :state="fieldState">
@ -17,7 +16,7 @@
</template>
<template #label="slotProps">
<slot name="label" v-bind="slotProps" :lang="'lang_'+id"></slot>
<slot v-bind="slotProps" name="label" :lang="'lang_'+id"></slot>
<span v-if="isRequired" class="text-danger">
<span aria-hidden="true">*</span>
<span class="sr-only">Required</span>
@ -27,12 +26,11 @@
</span>
</template>
<template #description="slotProps">
<slot name="description" v-bind="slotProps" :lang="'lang_'+id+'_desc'"></slot>
<slot v-bind="slotProps" name="description" :lang="'lang_'+id+'_desc'"></slot>
</template>
<slot v-for="(_, name) in $slots" :name="name" :slot="name"/>
<template v-for="(_, name) in filteredScopedSlots" :slot="name" slot-scope="slotData">
<slot :name="name" v-bind="slotData"/>
<template v-for="(_, slot) of filteredScopedSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot>
</template>
</b-form-group>
</template>
@ -101,7 +99,7 @@ export default {
}
},
filteredScopedSlots() {
return _.filter(this.$scopedSlots, (slot, name) => {
return _.filter(this.$slots, (slot, name) => {
return !_.includes([
'default', 'label', 'description'
], name);

View File

@ -10,7 +10,7 @@
</div>
<div class="break"></div>
<small class="date-played text-muted">
<span v-html="unixTimestampToDate(row.played_at)">{{ row.played_at }}</span>
<span v-html="unixTimestampToDate(row.played_at)"></span>
</small>
</div>
</div>

View File

@ -37,7 +37,7 @@
<div class="mt-3" v-if="playing">
<div class="d-flex flex-row mb-2">
<div class="flex-shrink-0 pt-1 pr-2">{{ position | prettifyTime }}</div>
<div class="flex-shrink-0 pt-1 pr-2">{{ prettifyTime(position) }}</div>
<div class="flex-fill">
<input type="range" min="0" max="100" step="0.1" class="custom-range slider"
v-bind:value="seekingPosition"
@ -45,7 +45,7 @@
v-on:mousemove="doSeek($event)"
v-on:mouseup="isSeeking = false">
</div>
<div class="flex-shrink-0 pt-1 pl-2">{{ duration | prettifyTime }}</div>
<div class="flex-shrink-0 pt-1 pl-2">{{ prettifyTime(duration) }}</div>
</div>
<div class="progress mb-1">
@ -93,7 +93,7 @@
<h5 class="mb-0">{{
rowFile.metadata.title ? rowFile.metadata.title : lang_unknown_title
}}</h5>
<small class="pt-1">{{ rowFile.audio.length | prettifyTime }}</small>
<small class="pt-1">{{ prettifyTime(rowFile.audio.length) }}</small>
</div>
<p class="mb-0">{{ rowFile.metadata.artist ? rowFile.metadata.artist : lang_unknown_artist }}</p>
</a>
@ -155,16 +155,16 @@ export default {
this.$root.$on('new-mixer-value', this.setMixGain);
this.$root.$on('new-cue', this.onNewCue);
},
filters: {
prettifyTime (time) {
methods: {
prettifyTime(time) {
if (typeof time === 'undefined') {
return 'N/A';
}
var hours = parseInt(time / 3600);
let hours = parseInt(time / 3600);
time %= 3600;
var minutes = parseInt(time / 60);
var seconds = parseInt(time % 60);
let minutes = parseInt(time / 60);
let seconds = parseInt(time % 60);
if (minutes < 10) {
minutes = '0' + minutes;
@ -178,19 +178,17 @@ export default {
} else {
return minutes + ':' + seconds;
}
}
},
methods: {
cue () {
},
cue() {
this.resumeStream();
this.$root.$emit('new-cue', (this.passThrough) ? 'off' : this.id);
},
onNewCue (new_cue) {
onNewCue(new_cue) {
this.passThrough = (new_cue === this.id);
},
setMixGain (new_value) {
setMixGain(new_value) {
if (this.id === 'playlist_1') {
this.mixGainObj.gain.value = 1.0 - new_value;
} else {

View File

@ -13,7 +13,7 @@
<translate key="lang_hdr_info">Continue the setup process by creating your first radio station below. You can edit any of these details later.</translate>
</info-card>
<admin-stations-form ref="form" v-bind="$props" :is-edit-mode="false" :create-url="createUrl"
<admin-stations-form v-bind="$props" ref="form" :is-edit-mode="false" :create-url="createUrl"
@submitted="onSubmitted">
<template #submitButtonText>
<translate key="lang_btn_create_and_continue">Create and Continue</translate>

View File

@ -24,7 +24,7 @@
<h5 class="m-0">{{ row.item.name }}</h5>
</template>
<template #cell(format)="row">
{{ row.item.format|upper }}
{{ upper(row.item.format) }}
</template>
<template #cell(bitrate)="row">
{{ row.item.bitrate }}kbps
@ -70,16 +70,14 @@ export default {
]
};
},
filters: {
methods: {
upper(data) {
let upper = [];
data.split(' ').forEach((word) => {
upper.push(word.toUpperCase());
});
return upper.join(' ');
}
},
methods: {
},
relist() {
this.$refs.datatable.refresh();
},

View File

@ -11,10 +11,14 @@ import {required} from '@vuelidate/validators';
import BaseEditModal from '~/components/Common/BaseEditModal';
import FormBasicInfo from './Form/BasicInfo';
import mergeExisting from "~/functions/mergeExisting";
import useVuelidate from "@vuelidate/core";
export default {
name: 'EditModal',
emits: ['needs-restart'],
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
components: {FormBasicInfo},
validations() {

View File

@ -46,7 +46,7 @@
</div>
<data-table ref="datatable" id="station_media" selectable paginated select-fields
@row-selected="onRowSelected" @refreshed="onRefreshed" :fields="fields" :api-url="listUrl"
@row-selected="onRowSelected" :fields="fields" :api-url="listUrl"
:request-config="requestConfig">
<template #cell(path)="row">
<div class="d-flex align-items-center">
@ -312,7 +312,7 @@ export default {
window.addEventListener('hashchange', this.onHashChange);
},
destroyed() {
unmounted() {
window.removeEventListener('hashchange', this.onHashChange);
},
computed: {
@ -342,9 +342,6 @@ export default {
directories: _.map(splitItems[0], 'path')
};
},
onRefreshed() {
this.$eventHub.$emit('refreshed');
},
onTriggerNavigate() {
this.$refs.datatable.navigate();
},
@ -367,9 +364,6 @@ export default {
isFilterString(str) {
return str.substring(0, 9) === 'playlist:' || str.substring(0, 8) === 'special:';
},
playAudio(url) {
this.$eventHub.$emit('player_toggle', url);
},
changeDirectory(newDir) {
window.location.hash = newDir;

View File

@ -1,6 +1,6 @@
<template>
<div class="row pt-4" id="app-toolbar">
<div class="col-md-8">
<div class="col-md-8 buttons">
<div class="btn-group dropdown allow-focus">
<b-dropdown size="sm" variant="primary" ref="setPlaylistsDropdown" v-b-tooltip.hover
:title="langPlaylistDropdown">

View File

@ -32,7 +32,7 @@
<template v-if="row.item.enable_autodj">
<translate key="lang_autodj_enabled">Enabled</translate>
-
{{ row.item.autodj_bitrate }}kbps {{ row.item.autodj_format|upper }}
{{ row.item.autodj_bitrate }}kbps {{ upper(row.item.autodj_format) }}
</template>
<template v-else>
<translate key="lang_autodj_disabled">Disabled</translate>
@ -86,16 +86,14 @@ export default {
]
};
},
filters: {
methods: {
upper(data) {
let upper = [];
data.split(' ').forEach((word) => {
upper.push(word.toUpperCase());
});
return upper.join(' ');
}
},
methods: {
},
relist() {
this.$refs.datatable.refresh();
},

View File

@ -26,10 +26,14 @@ import MountFormAutoDj from './Form/AutoDj';
import MountFormAdvanced from './Form/Advanced';
import MountFormIntro from "./Form/Intro";
import mergeExisting from "~/functions/mergeExisting";
import useVuelidate from "@vuelidate/core";
export default {
name: 'EditModal',
emits: ['needs-restart'],
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
components: {MountFormIntro, MountFormAdvanced, MountFormAutoDj, MountFormBasicInfo},
props: {

View File

@ -18,11 +18,15 @@ import FormBasicInfo from './Form/BasicInfo';
import FormSchedule from './Form/Schedule';
import FormAdvanced from './Form/Advanced';
import BaseEditModal from '~/components/Common/BaseEditModal';
import useVuelidate from "@vuelidate/core";
export default {
name: 'EditModal',
emits: ['needs-restart'],
components: {FormSchedule, FormBasicInfo, FormAdvanced},
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
props: {
stationTimeZone: String,

View File

@ -11,7 +11,7 @@
<playlists-form-schedule-row v-for="(row, index) in scheduleItems" :key="index"
:station-time-zone="stationTimeZone"
:index="index" :row.sync="row" @remove="remove(index)">
:index="index" v-model:row="scheduleItems[index]" @remove="remove(index)">
</playlists-form-schedule-row>
<b-button-group>

View File

@ -24,10 +24,14 @@ import PodcastCommonArtwork from './Common/Artwork';
import EpisodeFormMedia from './EpisodeForm/Media';
import {DateTime} from 'luxon';
import mergeExisting from "~/functions/mergeExisting";
import useVuelidate from "@vuelidate/core";
export default {
name: 'EditModal',
components: {EpisodeFormMedia, PodcastCommonArtwork, EpisodeFormBasicInfo},
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
props: {
stationTimeZone: String,

View File

@ -20,10 +20,14 @@ import BaseEditModal from '~/components/Common/BaseEditModal';
import PodcastFormBasicInfo from './PodcastForm/BasicInfo';
import PodcastCommonArtwork from './Common/Artwork';
import mergeExisting from "~/functions/mergeExisting";
import useVuelidate from "@vuelidate/core";
export default {
name: 'EditModal',
components: {PodcastCommonArtwork, PodcastFormBasicInfo},
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
props: {
stationTimeZone: String,

View File

@ -4,26 +4,26 @@
<div class="row" id="profile">
<div class="col-lg-7">
<profile-now-playing :np="np" v-bind="$props"></profile-now-playing>
<profile-now-playing v-bind="$props" :np="np"></profile-now-playing>
<profile-schedule :station-time-zone="stationTimeZone" :schedule-items="np.schedule"></profile-schedule>
<profile-streams :np="np" v-bind="$props"></profile-streams>
<profile-streams v-bind="$props" :np="np"></profile-streams>
<profile-public-pages v-bind="$props"></profile-public-pages>
</div>
<div class="col-lg-5">
<profile-requests v-if="stationSupportsRequests" v-bind="$props"></profile-requests>
<profile-requests v-bind="$props" v-if="stationSupportsRequests"></profile-requests>
<profile-streamers v-if="stationSupportsStreamers" v-bind="$props"></profile-streamers>
<profile-streamers v-bind="$props" v-if="stationSupportsStreamers"></profile-streamers>
<template v-if="hasActiveFrontend">
<profile-frontend :np="np" v-bind="$props"></profile-frontend>
<profile-frontend v-bind="$props" :np="np"></profile-frontend>
</template>
<template v-if="hasActiveBackend">
<profile-backend :np="np" v-bind="$props"></profile-backend>
<profile-backend v-bind="$props" :np="np"></profile-backend>
</template>
<template v-else>
<profile-backend-none></profile-backend-none>

View File

@ -5,10 +5,13 @@
<h3 class="flex-shrink card-title my-0" key="lang_profile_nowplaying_onair" v-translate>On the Air</h3>
<h6 class="card-subtitle text-right flex-fill my-0" style="line-height: 1;">
<icon class="sm align-middle" icon="headset"></icon>
{{ langListeners }}
<span class="pl-1">
{{ langListeners }}
</span>
<br>
<small>
<span>{{ np.listeners.unique }}</span>
<span class="pr-1">{{ np.listeners.unique }}</span>
<translate key="lang_profile_nowplaying_unique">Unique</translate>
</small>
</h6>

View File

@ -55,7 +55,7 @@
<translate key="lang_public_pages_disable">Disable</translate>
</a>
</div>
<embed-modal ref="embed_modal" v-bind="$props"></embed-modal>
<embed-modal v-bind="$props" ref="embed_modal"></embed-modal>
</template>
<template v-else>
<div class="card-header bg-primary-dark">

View File

@ -27,9 +27,9 @@
</td>
<td class="pl-1 text-right">
<icon class="sm align-middle" icon="headset"></icon>
<span class="listeners-total">{{ mount.listeners.total }}</span><br>
<span class="listeners-total pl-1">{{ mount.listeners.total }}</span><br>
<small>
<span class="listeners-unique">{{ mount.listeners.unique }}</span>
<span class="listeners-unique pr-1">{{ mount.listeners.unique }}</span>
<translate key="lang_streams_unique">Unique</translate>
</small>
</td>
@ -55,9 +55,9 @@
</td>
<td class="pl-1 text-right">
<icon class="sm align-middle" icon="headset"></icon>
<span class="listeners-total">{{ remote.listeners.total }}</span><br>
<span class="listeners-total pl-1">{{ remote.listeners.total }}</span><br>
<small>
<span class="listeners-unique">{{ remote.listeners.unique }}</span>
<span class="listeners-unique pr-1">{{ remote.listeners.unique }}</span>
<translate key="lang_streams_unique">Unique</translate>
</small>
</td>
@ -82,7 +82,7 @@
</td>
<td class="pl-1 text-right">
<icon class="sm align-middle" icon="headset"></icon>
<span class="listeners-total">{{ np.station.hls_listeners }}</span>
<span class="listeners-total pl-1">{{ np.station.hls_listeners }}</span>
</td>
</tr>
</tbody>

View File

@ -6,7 +6,7 @@
</h3>
</div>
<admin-stations-form ref="form" v-bind="$props" :is-edit-mode="true" :edit-url="editUrl"
<admin-stations-form v-bind="$props" ref="form" :is-edit-mode="true" :edit-url="editUrl"
@submitted="onSubmitted"></admin-stations-form>
</section>
</template>

View File

@ -15,6 +15,7 @@
<script>
import '~/vendor/clipboard.js';
import {copyToClipboard} from "~/vendor/clipboard";
export default {
name: 'QueueLogsModal',
@ -39,7 +40,7 @@ export default {
this.$refs.modal.show();
},
doCopy() {
this.$copyText(this.logs);
copyToClipboard(this.logs);
},
close() {
this.$refs.modal.hide();

View File

@ -28,7 +28,7 @@
<template v-if="row.item.enable_autodj">
<translate key="lang_autodj_enabled">Enabled</translate>
-
{{ row.item.autodj_bitrate }}kbps {{ row.item.autodj_format|upper }}
{{ row.item.autodj_bitrate }}kbps {{ upper(row.item.autodj_format) }}
</template>
<template v-else>
<translate key="lang_autodj_disabled">Disabled</translate>
@ -77,7 +77,7 @@ export default {
]
};
},
filters: {
methods: {
upper(data) {
if (!data) {
return '';
@ -88,9 +88,7 @@ export default {
upper.push(word.toUpperCase());
});
return upper.join(' ');
}
},
methods: {
},
relist() {
this.$refs.datatable.refresh();
},

View File

@ -16,10 +16,14 @@ import BaseEditModal from '~/components/Common/BaseEditModal';
import RemoteFormBasicInfo from "./Form/BasicInfo";
import RemoteFormAutoDj from "./Form/AutoDj";
import {REMOTE_ICECAST} from "~/components/Entity/RadioAdapters";
import useVuelidate from "@vuelidate/core";
export default {
name: 'RemoteEditModal',
emits: ['needs-restart'],
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
components: {
RemoteFormAutoDj,

View File

@ -9,14 +9,15 @@
<translate key="lang_header">Listeners</translate>
</h2>
</div>
<div class="flex-shrink">
<div class="flex-shrink buttons">
<a class="btn btn-bg" id="btn-export" :href="exportUrl" target="_blank">
<icon icon="file_download"></icon>
<translate key="lang_download_csv_button">Download CSV</translate>
</a>
<date-range-dropdown v-if="!isLive" time-picker :min-date="minDate" :max-date="maxDate"
:tz="stationTimeZone" v-model="dateRange" @update="updateListeners">
:tz="stationTimeZone" v-model="dateRange"
@update="updateListeners">
</date-range-dropdown>
</div>
</div>

View File

@ -0,0 +1,61 @@
<template>
<div id="leaflet-container" ref="map">
<slot v-if="$map" :map="$map"/>
</div>
</template>
<style lang="scss">
@import 'leaflet/dist/leaflet.css';
.leaflet-container {
height: 300px;
z-index: 0;
}
</style>
<script setup>
import {onMounted, provide, ref} 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();
provide('map', $map);
onMounted(() => {
// Fix issue with Leaflet icons being built in Webpack
// https://github.com/Leaflet/Leaflet/issues/4968
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
});
// Init map
const map = L.map(get($container));
map.setView([40, 0], 1);
set($map, map);
// Add tile layer
const tileUrl = 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/{theme}_all/{z}/{x}/{y}.png';
const tileAttribution = 'Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.';
L.tileLayer(tileUrl, {
theme: App.theme,
attribution: tileAttribution,
}).addTo(map);
/*
// Add fullscreen control
const fullscreenControl = new L.Control.Fullscreen();
map.addControl(fullscreenControl)
*/
});
</script>

View File

@ -1,66 +1,38 @@
<template>
<l-map v-if="mapPoints.length < 3000" style="height: 300px; z-index: 0;" :zoom="1" :center="[40, 0]">
<l-control-fullscreen/>
<l-tile-layer :url="tileUrl" :attribution="tileAttribution"></l-tile-layer>
<l-marker v-for="l in mapPoints" :key="l.hash"
:lat-lng="{lat: l.location.lat, lng: l.location.lon}">
<l-tooltip>
IP: {{ l.ip }}<br>
Country: {{ l.location.country }}<br>
Region: {{ l.location.region }}<br>
City: {{ l.location.city }}<br>
Time connected: {{ l.connected_time }}<br>
User Agent: {{ l.user_agent }}
</l-tooltip>
</l-marker>
</l-map>
<inner-map v-if="visibleListeners.length < 3000"
:attribution="attribution">
<map-point v-for="l in visibleListeners" :key="l.hash"
:position="[l.location.lat, l.location.lon]">
<translate key="l-ip">IP</translate>
: {{ l.ip }}<br>
<translate key="l-country">Country</translate>
: {{ l.location.country }}<br>
<translate key="l-region">Region</translate>
: {{ l.location.region }}<br>
<translate key="l-city">City</translate>
: {{ l.location.city }}<br>
<translate key="l-time">Time</translate>
: {{ l.connected_time }}<br>
<translate key="l-ua">User Agent</translate>
: {{ l.user_agent }}
</map-point>
</inner-map>
</template>
<style lang="css">
@import '../../../../../node_modules/leaflet/dist/leaflet.css';
</style>
<script setup>
import InnerMap from "./InnerMap.vue";
import MapPoint from "./MapPoint.vue";
import {computed} from "vue";
import _ from "lodash";
<script>
import L from 'leaflet';
import {LMap, LMarker, LTileLayer, LTooltip} from 'vue2-leaflet';
import LControlFullscreen from 'vue2-leaflet-fullscreen';
import _ from 'lodash';
const props = defineProps({
attribution: String,
listeners: Array,
});
export default {
name: 'StationReportsListenersMap',
props: {
attribution: String,
listeners: Array,
},
components: {
LMap,
LTileLayer,
LMarker,
LTooltip,
LControlFullscreen
},
data() {
return {
tileUrl: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/' + App.theme + '_all/{z}/{x}/{y}.png',
tileAttribution: 'Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL.',
}
},
mounted() {
// Fix issue with Leaflet icons being built in Webpack
// https://github.com/Leaflet/Leaflet/issues/4968
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
});
},
computed: {
mapPoints() {
return _.filter(this.listeners, function (l) {
return null !== l.location.lat && null !== l.location.lon;
});
}
}
};
const visibleListeners = computed(() => {
return _.filter(props.listeners, function (l) {
return null !== l.location.lat && null !== l.location.lon;
});
});
</script>

View File

@ -0,0 +1,37 @@
<template>
<div ref="popup-content">
<slot/>
</div>
</template>
<script setup>
import {get, set, templateRef} from '@vueuse/core';
import {inject, onUnmounted, ref, toRaw, watch} from 'vue';
const props = defineProps({
position: Array
});
const $map = inject('map');
const $marker = ref();
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');
watch(
$popupContent,
(content) => {
popup.setContent(content);
marker.bindPopup(popup);
},
{immediate: true}
);
onUnmounted(() => {
get($marker).remove();
});
</script>

View File

@ -6,7 +6,8 @@
<translate key="hdr">Station Statistics</translate>
</h2>
<div class="flex-shrink">
<date-range-dropdown time-picker v-model="dateRange" :tz="stationTimeZone"></date-range-dropdown>
<date-range-dropdown time-picker v-model="dateRange"
:tz="stationTimeZone"></date-range-dropdown>
</div>
</div>
</div>

View File

@ -4,71 +4,68 @@
</canvas>
</template>
<script>
import {Chart} from 'chart.js';
import {Tableau20} from '~/vendor/chartjs-colorschemes/colorschemes.tableau.js';
<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";
export default {
name: 'HourChart',
inheritAttrs: true,
props: {
options: Object,
data: Array,
labels: Array
},
data() {
return {
_chart: null
};
},
mounted() {
this.renderChart();
},
methods: {
renderChart() {
const defaultOptions = {
type: 'bar',
data: {
labels: this.labels,
datasets: this.data
const props = defineProps({
options: Object,
data: Array,
labels: Array
});
let $chart = null;
const $canvas = templateRef('canvas');
const {$gettext} = gettext;
watchOnce($canvas, () => {
const defaultOptions = {
type: 'bar',
data: {
labels: props.labels,
datasets: props.data
},
options: {
aspectRatio: 2,
plugins: {
colorschemes: {
scheme: Tableau20
}
},
scales: {
x: {
scaleLabel: {
display: true,
labelString: $gettext('Hour')
}
},
options: {
aspectRatio: 2,
plugins: {
colorschemes: {
scheme: Tableau20
}
y: {
scaleLabel: {
display: true,
labelString: $gettext('Listeners')
},
scales: {
x: {
scaleLabel: {
display: true,
labelString: this.$gettext('Hour')
}
},
y: {
scaleLabel: {
display: true,
labelString: this.$gettext('Listeners')
},
ticks: {
min: 0
}
}
ticks: {
min: 0
}
}
};
if (this._chart) this._chart.destroy();
let chartOptions = _.defaultsDeep({}, this.options, defaultOptions);
this._chart = new Chart(this.$refs.canvas.getContext('2d'), chartOptions);
}
},
beforeDestroy() {
if (this._chart) {
this._chart.destroy();
}
}
};
if ($chart) {
$chart.destroy();
}
};
let chartOptions = _.defaultsDeep({}, props.options, defaultOptions);
$chart = new Chart(get($canvas).getContext('2d'), chartOptions);
});
onUnmounted(() => {
if ($chart) {
$chart.destroy();
}
});
</script>

View File

@ -22,6 +22,7 @@
nonsubscription transmissions other than broadcast simulcasts and transmissions of non-music
programming." If your station does not fall within this category, update the transmission
category field accordingly.
</li>
<li>The data collected by AzuraCast meets the SoundExchange standard for Actual Total
Performances (ATP) by tracking unique listeners across all song plays. All other information
is derived from the metadata of the uploaded songs themselves, and may not be completely

View File

@ -10,9 +10,13 @@
import {required} from '@vuelidate/validators';
import BaseEditModal from '~/components/Common/BaseEditModal';
import SftpUsersForm from "./Form";
import useVuelidate from "@vuelidate/core";
export default {
name: 'SftpUsersEditModal',
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
components: {SftpUsersForm},
validations() {

View File

@ -19,9 +19,13 @@ import FormSchedule from './Form/Schedule';
import FormArtwork from './Form/Artwork';
import BaseEditModal from '~/components/Common/BaseEditModal';
import mergeExisting from "~/functions/mergeExisting";
import useVuelidate from "@vuelidate/core";
export default {
name: 'EditModal',
setup() {
return {v$: useVuelidate()}
},
mixins: [BaseEditModal],
components: {FormBasicInfo, FormSchedule, FormArtwork},
props: {

View File

@ -12,7 +12,7 @@
<streamers-form-schedule-row v-for="(row, index) in scheduleItems" :key="index"
:station-time-zone="stationTimeZone"
:index="index" :row.sync="row" @remove="remove(index)">
:index="index" v-model:row="scheduleItems[index]" @remove="remove(index)">
</streamers-form-schedule-row>
<b-button-group>

View File

@ -33,9 +33,13 @@ import Twitter from "./Form/Twitter";
import GoogleAnalytics from "./Form/GoogleAnalytics";
import MatomoAnalytics from "./Form/MatomoAnalytics";
import Mastodon from "./Form/Mastodon";
import useVuelidate from "@vuelidate/core";
export default {
name: 'EditModal',
setup() {
return {v$: useVuelidate()}
},
components: {BasicInfo, TypeSelect},
mixins: [BaseEditModal],
props: {

View File

@ -1,4 +1,6 @@
export default function (seconds) {
seconds = parseInt(seconds);
let d = Math.floor(seconds / 86400),
h = Math.floor(seconds / 3600) % 24,
m = Math.floor(seconds / 60) % 60,
@ -6,6 +8,6 @@ export default function (seconds) {
return (d > 0 ? d + 'd ' : '')
+ (h > 0 ? ('0' + h).slice(-2) + ':' : '')
+ (m > 0 ? ('0' + m).slice(-2) + ':' : '')
+ (seconds > 60 ? ('0' + s).slice(-2) : s);
+ ('0' + m).slice(-2) + ':'
+ ('0' + s).slice(-2);
}

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import Account from '~/components/Account';
export default initBase(Account);

View File

@ -1,8 +1,4 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminApiKeys from '~/components/Admin/ApiKeys.vue';
export default initBase(AdminApiKeys);

View File

@ -1,10 +1,6 @@
import initBase
from '~/base.js';
import '~/vendor/bootstrapVue.js';
import initBase from '~/base.js';
import '~/vendor/luxon.js';
import AuditLog
from '~/components/Admin/AuditLog.vue';
import AuditLog from '~/components/Admin/AuditLog.vue';
export default initBase(AuditLog);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminBackups from '~/components/Admin/Backups.vue';
export default initBase(AdminBackups);

View File

@ -1,10 +1,7 @@
import initBase
from '~/base.js';
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/fancybox.js';
import AdminBranding
from '~/components/Admin/Branding.vue';
import AdminBranding from '~/components/Admin/Branding.vue';
export default initBase(AdminBranding);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminCustomFields from '~/components/Admin/CustomFields.vue';
export default initBase(AdminCustomFields);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import AdminGeoLite from '~/components/Admin/GeoLite.vue';
export default initBase(AdminGeoLite);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminIndex from '~/components/Admin/Index.vue';
export default initBase(AdminIndex);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import AdminLogs from '~/components/Admin/Logs.vue';
export default initBase(AdminLogs);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminPermissions from '~/components/Admin/Permissions.vue';
export default initBase(AdminPermissions);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import AdminSettings from '~/components/Admin/Settings.vue';
export default initBase(AdminSettings);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import AdminShoutcast from '~/components/Admin/Shoutcast.vue';
export default initBase(AdminShoutcast);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminStations from '~/components/Admin/Stations.vue';
export default initBase(AdminStations);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import AdminStereoTool from '~/components/Admin/StereoTool.vue';
export default initBase(AdminStereoTool);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import StorageLocations from '~/components/Admin/StorageLocations.vue';
export default initBase(StorageLocations);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import AdminUsers from '~/components/Admin/Users.vue';
export default initBase(AdminUsers);

View File

@ -1,6 +1,4 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/fancybox.js';
import '~/vendor/chartjs.js';

View File

@ -1,11 +1,20 @@
import Vue from 'vue';
import {createApp} from 'vue';
import InlinePlayer from '~/components/InlinePlayer.vue';
import pinia from '../vendor/pinia';
import usePinia from '../vendor/pinia';
import gettext from "../vendor/gettext";
document.addEventListener('DOMContentLoaded', function () {
let inlinePlayer = new Vue({
el: '#radio-player-controls',
render: createElement => createElement(InlinePlayer),
pinia
});
const inlineApp = createApp(InlinePlayer);
/* Gettext */
if (typeof App.locale !== 'undefined') {
inlineApp.config.language = App.locale;
}
inlineApp.use(gettext);
/* Pinia */
usePinia(inlineApp);
inlineApp.mount('#radio-player-controls');
});

View File

@ -1,6 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/fancybox.js';
import '~/vendor/luxon.js';

View File

@ -1,9 +1,5 @@
import initBase
from '~/base.js';
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import OnDemand
from '~/components/Public/OnDemand.vue';
import OnDemand from '~/components/Public/OnDemand.vue';
export default initBase(OnDemand);

View File

@ -1,9 +1,5 @@
import initBase
from '~/base.js';
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import Requests
from '~/components/Public/Requests.vue';
import Requests from '~/components/Public/Requests.vue';
export default initBase(Requests);

View File

@ -1,10 +1,7 @@
import initBase
from '~/base.js';
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/luxon.js';
import Schedule
from '~/components/Public/Schedule.vue';
import Schedule from '~/components/Public/Schedule.vue';
export default initBase(Schedule);

View File

@ -1,9 +1,5 @@
import initBase
from '~/base.js';
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import WebDJ
from '~/components/Public/WebDJ.vue';
import WebDJ from '~/components/Public/WebDJ.vue';
export default initBase(WebDJ);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import Recover from '~/components/Recover.vue';
export default initBase(Recover);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import SetupRegister from '~/components/Setup/Register.vue';
export default initBase(SetupRegister);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import SetupSettings from '~/components/Setup/Settings.vue';
export default initBase(SetupSettings);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import SetupStation from '~/components/Setup/Station.vue';
export default initBase(SetupStation);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import BulkMedia from '~/components/Stations/BulkMedia.vue';
export default initBase(BulkMedia);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import Fallback from '~/components/Stations/Fallback.vue';
export default initBase(Fallback);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import Help from '~/components/Stations/Help.vue';
export default initBase(Help);

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import HlsStreams from '~/components/Stations/HlsStreams.vue';
export default initBase(HlsStreams);

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import LiquidsoapConfig from '~/components/Stations/LiquidsoapConfig.vue';
export default initBase(LiquidsoapConfig);

View File

@ -1,9 +1,7 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/fancybox.js';
import '~/vendor/luxon.js';
import '~/vendor/sweetalert.js';
import '~/pages/InlinePlayer.js';

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import Mounts from '~/components/Stations/Mounts.vue';
export default initBase(Mounts);

View File

@ -1,8 +1,6 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/luxon.js';
import '~/vendor/sweetalert.js';
import '~/store';
import Playlists from '~/components/Stations/Playlists.vue';

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import '~/vendor/fancybox.js';
import '~/vendor/luxon.js';

View File

@ -1,6 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/fancybox.js';
import '~/vendor/luxon.js';

View File

@ -1,7 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import ProfileEdit from '~/components/Stations/ProfileEdit.vue';
export default initBase(ProfileEdit);

View File

@ -1,8 +1,6 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/luxon.js';
import '~/vendor/sweetalert.js';
import Queue from '~/components/Stations/Queue.vue';

View File

@ -1,8 +1,5 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import Remotes from '~/components/Stations/Remotes.vue';
export default initBase(Remotes);

Some files were not shown because too many files have changed in this diff Show More