Vue 3 WIP

This commit is contained in:
Buster Neece 2022-12-10 11:55:09 -06:00
parent 37dc95b7d9
commit a4e1f15e47
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
38 changed files with 1202 additions and 695 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,7 +16,9 @@
"@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-leaflet/vue-leaflet": "*",
"@vue/compat": "^3.2.45",
"@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0",
"axios": "^1",
@ -61,14 +63,12 @@
"sass-loader": "^13",
"store": "^2",
"sweetalert2": "11.4.8",
"vue": "^2 <3",
"vue-axios": "^2 <3",
"vue": "^3.2",
"vue-axios": "^3.5",
"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-loader": "^17",
"vue3-daterange-picker": "^1",
"vuedraggable": "^2.24.1",
"wavesurfer.js": "^6",
"webpack": "^5.52.1",

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,70 +1,65 @@
import Vue from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios';
import GetTextPlugin from 'vue-gettext';
import translations from '../../translations/translations.json';
document.addEventListener('DOMContentLoaded', function () {
// Configure localization
Vue.use(GetTextPlugin, {
defaultLanguage: 'en_US',
translations: translations,
silent: true
});
export default function (vueApp) {
return function (el, props) {
document.addEventListener('DOMContentLoaded', function () {
// Configure localization
vueApp.use(GetTextPlugin, {
defaultLanguage: 'en_US',
translations: translations,
silent: true
});
if (typeof App.locale !== 'undefined') {
Vue.config.language = App.locale;
}
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;
}
// Configure auto-CSRF on requests
if (typeof App.api_csrf !== 'undefined') {
axios.defaults.headers.common['X-API-CSRF'] = App.api_csrf;
}
Vue.use(VueAxios, axios);
Vue.prototype.$eventHub = new Vue();
});
export default function (component) {
return function (el, props) {
return new Vue({
el: el,
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);
}
if (typeof this.$notifyError === 'function') {
this.$notifyError(notifyMessage);
}
};
axios.interceptors.request.use((config) => {
return config;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
vueApp.use(VueAxios, axios);
});
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
handleAxiosError(error);
return Promise.reject(error);
vueApp.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);
}
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);
});
});
},
render: createElement => createElement(component, { props: props })
});
};
vueApp.mount(el, {props: props});
};
}

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

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

View File

@ -1,7 +1,6 @@
<template>
<date-range-picker
ref="picker" controlContainerClass="" opens="left" show-dropdowns
v-bind="$props"
v-bind="$props" ref="picker" controlContainerClass="" opens="left" show-dropdowns
:time-picker-increment="1" :ranges="ranges" @update="onUpdate">
<template #input="datePicker">
<a class="btn btn-bg dropdown-toggle" id="reportrange" href="#" @click.prevent="">
@ -9,19 +8,19 @@
{{ 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 '../../../node_modules/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';

View File

@ -1,16 +1,18 @@
<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>

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

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

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

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

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

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

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

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

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

@ -1,6 +1,5 @@
<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}">
@ -22,8 +21,7 @@
<script>
import L from 'leaflet';
import {LMap, LMarker, LTileLayer, LTooltip} from 'vue2-leaflet';
import LControlFullscreen from 'vue2-leaflet-fullscreen';
import {LMap, LMarker, LTileLayer, LTooltip} from '@vue-leaflet/vue-leaflet';
import _ from 'lodash';
export default {
@ -36,8 +34,7 @@ export default {
LMap,
LTileLayer,
LMarker,
LTooltip,
LControlFullscreen
LTooltip
},
data() {
return {

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

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

@ -1,8 +1,13 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import '~/vendor/sweetalert.js';
import {createApp} from "vue";
import useSweetAlert from '~/vendor/sweetalert.js';
import useBootstrapVue from '~/vendor/bootstrapVue.js';
import AdminApiKeys from '~/components/Admin/ApiKeys.vue';
export default initBase(AdminApiKeys);
const vueApp = createApp(AdminApiKeys);
useSweetAlert(vueApp);
useBootstrapVue(vueApp);
export default initBase(vueApp);

View File

@ -1,10 +1,12 @@
import initBase
from '~/base.js';
import '~/vendor/bootstrapVue.js';
import initBase from '~/base.js';
import {createApp} from "vue";
import useBootstrapVue from '~/vendor/bootstrapVue.js';
import '~/vendor/luxon.js';
import AuditLog
from '~/components/Admin/AuditLog.vue';
import AuditLog from '~/components/Admin/AuditLog.vue';
export default initBase(AuditLog);
const vueApp = createApp(AuditLog);
useBootstrapVue(vueApp);
export default initBase(vueApp);

View File

@ -6,6 +6,8 @@ Vue.use(PiniaVuePlugin);
const pinia = createPinia();
document.addEventListener('DOMContentLoaded', function () {
let inlinePlayer = new Vue({
el: '#radio-player-controls',
render: createElement => createElement(InlinePlayer),

View File

@ -1,111 +1,106 @@
import Vue from 'vue';
import {BootstrapVue} from 'bootstrap-vue';
import 'bootstrap-vue/dist/bootstrap-vue.css';
Vue.use(BootstrapVue);
export default function useBootstrapVue(vueApp) {
vueApp.use(BootstrapVue);
const BootstrapVueNotifiers = {
install(Vue, opts) {
Vue.prototype.$notify = function (message = null, options = {}) {
if (!!document.hidden) {
return;
}
const defaults = {
variant: 'default',
toaster: 'b-toaster-top-right',
autoHideDelay: 3000,
solid: true
};
vueApp.config.globalProperties.$notify = function (message = null, options = {}) {
if (!!document.hidden) {
return;
}
this.$bvToast.toast(message, {...defaults, ...options});
const defaults = {
variant: 'default',
toaster: 'b-toaster-top-right',
autoHideDelay: 3000,
solid: true
};
Vue.prototype.$notifyError = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('An error occurred and your request could not be completed.');
}
this.$bvToast.toast(message, {...defaults, ...options});
};
const defaults = {
variant: 'danger',
title: this.$gettext('Error')
};
vueApp.config.globalProperties.$notifyError = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('An error occurred and your request could not be completed.');
}
this.$notify(message, {...defaults, ...options});
return message;
const defaults = {
variant: 'danger',
title: this.$gettext('Error')
};
Vue.prototype.$notifySuccess = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('Changes saved.');
}
this.$notify(message, {...defaults, ...options});
const defaults = {
variant: 'success',
title: this.$gettext('Success')
};
return message;
};
this.$notify(message, {...defaults, ...options});
vueApp.config.globalProperties.$notifySuccess = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('Changes saved.');
}
return message;
const defaults = {
variant: 'success',
title: this.$gettext('Success')
};
const LOADING_TOAST_ID = 'toast-loading';
this.$notify(message, {...defaults, ...options});
Vue.prototype.$showLoading = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('Applying changes...');
}
return message;
};
const defaults = {
id: LOADING_TOAST_ID,
variant: 'warning',
title: this.$gettext('Please wait...'),
autoHideDelay: 10000,
isStatus: true
};
const LOADING_TOAST_ID = 'toast-loading';
this.$notify(message, {...defaults, ...options});
return message;
vueApp.config.globalProperties.$showLoading = function (message = null, options = {}) {
if (message === null) {
message = this.$gettext('Applying changes...');
}
const defaults = {
id: LOADING_TOAST_ID,
variant: 'warning',
title: this.$gettext('Please wait...'),
autoHideDelay: 10000,
isStatus: true
};
Vue.prototype.$hideLoading = function () {
this.$bvToast.hide(LOADING_TOAST_ID);
};
this.$notify(message, {...defaults, ...options});
return message;
};
let $isAxiosLoading = false;
let $axiosLoadCount = 0;
vueApp.config.globalProperties.$hideLoading = function () {
this.$bvToast.hide(LOADING_TOAST_ID);
};
Vue.prototype.$setLoading = function (isLoading) {
let prevIsLoading = $isAxiosLoading;
if (isLoading) {
$axiosLoadCount++;
$isAxiosLoading = true;
} else if ($axiosLoadCount > 0) {
$axiosLoadCount--;
$isAxiosLoading = ($axiosLoadCount > 0);
}
let $isAxiosLoading = false;
let $axiosLoadCount = 0;
// Handle state changes
if (!prevIsLoading && $isAxiosLoading) {
this.$showLoading();
} else if (prevIsLoading && !$isAxiosLoading) {
this.$hideLoading();
}
};
vueApp.config.globalProperties.$setLoading = function (isLoading) {
let prevIsLoading = $isAxiosLoading;
if (isLoading) {
$axiosLoadCount++;
$isAxiosLoading = true;
} else if ($axiosLoadCount > 0) {
$axiosLoadCount--;
$isAxiosLoading = ($axiosLoadCount > 0);
}
Vue.prototype.$wrapWithLoading = function (promise) {
this.$setLoading(true);
// Handle state changes
if (!prevIsLoading && $isAxiosLoading) {
this.$showLoading();
} else if (prevIsLoading && !$isAxiosLoading) {
this.$hideLoading();
}
};
promise.finally(() => {
this.$setLoading(false);
});
vueApp.config.globalProperties.$wrapWithLoading = function (promise) {
this.$setLoading(true);
return promise;
};
}
promise.finally(() => {
this.$setLoading(false);
});
return promise;
};
};
Vue.use(BootstrapVueNotifiers);

View File

@ -1,8 +1,7 @@
import Vue
from 'vue';
import VueClipboard
from 'vue-clipboard2';
import VueClipboard from 'vue-clipboard2';
VueClipboard.config.autoSetContainer = true;
Vue.use(VueClipboard);
export default function useVueClipboard(vueApp) {
vueApp.use(VueClipboard);
};

View File

@ -1,4 +1,5 @@
import { Settings } from 'luxon';
import {Settings} from 'luxon';
document.addEventListener('DOMContentLoaded', function () {
Settings.defaultLocale = App.locale_with_dashes;

View File

@ -1,25 +1,19 @@
import Swal from 'sweetalert2';
import Vue from 'vue';
export default function useSweetAlert(vueApp) {
vueApp.config.globalProperties.$swal = function (options = {}) {
return Swal.fire(options);
};
const ConfirmFunctions = {
install(Vue, opts) {
Vue.prototype.$swal = function (options = {}) {
return Swal.fire(options);
vueApp.config.globalProperties.$confirmDelete = function (options = {}) {
const defaults = {
title: this.$gettext('Delete Record?'),
confirmButtonText: this.$gettext('Delete'),
confirmButtonColor: '#e64942',
showCancelButton: true,
focusCancel: true
};
Vue.prototype.$confirmDelete = function (options = {}) {
const defaults = {
title: this.$gettext('Delete Record?'),
confirmButtonText: this.$gettext('Delete'),
confirmButtonColor: '#e64942',
showCancelButton: true,
focusCancel: true
};
return this.$swal({...defaults, ...options});
};
}
};
Vue.use(ConfirmFunctions);
return this.$swal({...defaults, ...options});
};
}

View File

@ -60,7 +60,8 @@ module.exports = {
resolve: {
enforceExtension: false,
alias: {
'~': path.resolve(__dirname, './vue')
'~': path.resolve(__dirname, './vue'),
vue: '@vue/compat'
},
extensions: ['.js', '.vue', '.json']
},
@ -101,14 +102,18 @@ module.exports = {
rules: [
{
test: /\.vue$/i,
use: [
'vue-loader'
]
loader: 'vue-loader',
options: {
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
},
{
test: /\.scss$/i,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
@ -116,7 +121,6 @@ module.exports = {
{
test: /\.css$/i,
use: [
'vue-style-loader',
'css-loader'
]
},