4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-14 13:16:37 +00:00

More composition, make MayNeedRestart composable.

This commit is contained in:
Buster Neece 2022-12-25 05:44:59 -06:00
parent 48bf6352f4
commit d2cb74095a
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
21 changed files with 714 additions and 736 deletions

View File

@ -26,21 +26,19 @@
<streaming-log-modal ref="modal"></streaming-log-modal> <streaming-log-modal ref="modal"></streaming-log-modal>
</template> </template>
<script> <script setup>
import LogList from "~/components/Common/LogList"; import LogList from "~/components/Common/LogList";
import StreamingLogModal from "~/components/Common/StreamingLogModal"; import StreamingLogModal from "~/components/Common/StreamingLogModal";
import {ref} from "vue";
export default { const props = defineProps({
name: 'AdminLogs', systemLogsUrl: String,
components: {StreamingLogModal, LogList}, stationLogs: Array
props: { });
systemLogsUrl: String,
stationLogs: Array const modal = ref(); // StreamingLogModal
},
methods: { const viewLog = (url) => {
viewLog(url) { modal.value.show(url);
this.$refs.modal.show(url); };
}
}
}
</script> </script>

View File

@ -70,40 +70,39 @@
</div> </div>
</template> </template>
<script> <script setup>
import FlowUpload from "~/components/Common/FlowUpload"; import FlowUpload from "~/components/Common/FlowUpload";
import {computed, onMounted, ref} from "vue";
import {useTranslate} from "~/vendor/gettext";
import {useAxios} from "~/vendor/axios";
export default { const props = defineProps({
name: 'AdminShoutcast', apiUrl: String
components: {FlowUpload}, });
props: {
apiUrl: String const loading = ref(true);
}, const version = ref(null);
data() {
return { const {$gettext} = useTranslate();
loading: true,
version: null, const langInstalledVersion = computed(() => {
}; return $gettext(
}, 'Shoutcast version "%{ version }" is currently installed.',
computed: { {
langInstalledVersion() { version: version.value
const text = this.$gettext('Shoutcast version "%{ version }" is currently installed.');
return this.$gettextInterpolate(text, {
version: this.version
});
} }
}, );
mounted() { });
this.relist();
}, const {axios} = useAxios();
methods: {
relist() { const relist = () => {
this.loading = true; loading.value = true;
this.axios.get(this.apiUrl).then((resp) => { axios.get(props.apiUrl).then((resp) => {
this.version = resp.data.version; version.value = resp.data.version;
this.loading = false; loading.value = false;
}); });
} };
}
} onMounted(relist);
</script> </script>

View File

@ -77,43 +77,46 @@
</div> </div>
</template> </template>
<script> <script setup>
import FlowUpload from "~/components/Common/FlowUpload"; import FlowUpload from "~/components/Common/FlowUpload";
import {computed, onMounted, ref} from "vue";
import {useTranslate} from "~/vendor/gettext";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default { const props = defineProps({
name: 'AdminStereoTool', apiUrl: String
components: {FlowUpload}, });
props: {
apiUrl: String const loading = ref(true);
}, const version = ref(null);
data() {
return { const {$gettext} = useTranslate();
loading: true,
version: null, const langInstalledVersion = computed(() => {
}; return $gettext(
}, 'Stereo Tool version %{ version } is currently installed.',
computed: { {
langInstalledVersion() { version: version.value
const text = this.$gettext('Stereo Tool version %{ version } is currently installed.');
return this.$gettextInterpolate(text, {
version: this.version
});
} }
}, );
mounted() { });
this.relist();
}, const {notifyError} = useNotify();
methods: {
onError(file, message) { const onError = (file, message) => {
this.$notifyError(message); notifyError(message);
}, };
relist() {
this.loading = true; const {axios} = useAxios();
this.axios.get(this.apiUrl).then((resp) => {
this.version = resp.data.version; const relist = () => {
this.loading = false; loading.value = true;
}); axios.get(props.apiUrl).then((resp) => {
} version.value = resp.data.version;
} loading.value = false;
} });
};
onMounted(relist);
</script> </script>

View File

@ -13,34 +13,21 @@
<slot name="description" v-bind="slotProps"></slot> <slot name="description" v-bind="slotProps"></slot>
</template> </template>
<template v-for="(_, slot) of filteredScopedSlots" v-slot:[slot]="scope"> <template v-for="(_, slot) of filteredSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot> <slot :name="slot" v-bind="scope"></slot>
</template> </template>
</b-form-group> </b-form-group>
</template> </template>
<script> <script setup>
import _ from "lodash"; import useSlotsExcept from "~/functions/useSlotsExcept";
export default { const props = defineProps({
name: 'BFormMarkup', id: {
props: { type: String,
id: { required: true
type: String,
required: true
},
},
computed: {
filteredScopedSlots() {
return _.filter(this.$slots, (slot, name) => {
return !_.includes([
'default', 'label', 'description'
], name);
});
},
isRequired() {
return _.has(this.field, 'required');
}
} }
} });
const filteredSlots = useSlotsExcept(['default', 'label', 'description']);
</script> </script>

View File

@ -23,56 +23,49 @@
<slot name="description" v-bind="slotProps"></slot> <slot name="description" v-bind="slotProps"></slot>
</template> </template>
<template v-for="(_, slot) of filteredScopedSlots" v-slot:[slot]="scope"> <template v-for="(_, slot) of filteredSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot> <slot :name="slot" v-bind="scope"></slot>
</template> </template>
</b-form-group> </b-form-group>
</template> </template>
<script> <script setup>
import _ from "lodash"; import {has} from "lodash";
import VuelidateError from "./VuelidateError"; import VuelidateError from "./VuelidateError";
import useSlotsExcept from "~/functions/useSlotsExcept";
import {computed} from "vue";
export default { const props = defineProps({
name: 'BWrappedFormCheckbox', id: {
components: {VuelidateError}, type: String,
props: { required: true
id: { },
type: String, name: {
required: true type: String,
}, },
name: { field: {
type: String, type: Object,
}, required: true
field: { },
type: Object, inputAttrs: {
required: true type: Object,
}, default() {
inputAttrs: { return {};
type: Object,
default() {
return {};
}
},
advanced: {
type: Boolean,
default: false
} }
}, },
computed: { advanced: {
filteredScopedSlots() { type: Boolean,
return _.filter(this.$slots, (slot, name) => { default: false
return !_.includes([
'default', 'description'
], name);
});
},
fieldState() {
return this.field.$dirty ? !this.field.$error : null;
},
isRequired() {
return _.has(this.field, 'required');
}
} }
} });
const filteredSlots = useSlotsExcept(['default', 'description']);
const fieldState = computed(() => {
return props.field.$dirty ? !props.field.$error : null;
});
const isRequired = computed(() => {
return has(props.field, 'required');
});
</script> </script>

View File

@ -29,98 +29,96 @@
<slot v-bind="slotProps" name="description"></slot> <slot v-bind="slotProps" name="description"></slot>
</template> </template>
<template v-for="(_, slot) of filteredScopedSlots" v-slot:[slot]="scope"> <template v-for="(_, slot) of filteredSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope"></slot> <slot :name="slot" v-bind="scope"></slot>
</template> </template>
</b-form-group> </b-form-group>
</template> </template>
<script> <script setup>
import _ from "lodash";
import VuelidateError from "./VuelidateError"; import VuelidateError from "./VuelidateError";
import {computed, ref} from "vue";
import useSlotsExcept from "~/functions/useSlotsExcept";
import {has} from "lodash";
export default { const props = defineProps({
name: 'BWrappedFormGroup', id: {
components: {VuelidateError}, type: String,
props: { required: true
id: { },
type: String, name: {
required: true type: String,
}, },
name: { field: {
type: String, type: Object,
}, required: true
field: { },
type: Object, inputType: {
required: true type: String,
}, default: 'text'
inputType: { },
type: String, inputNumber: {
default: 'text' type: Boolean,
}, default: false
inputNumber: { },
type: Boolean, inputTrim: {
default: false type: Boolean,
}, default: false
inputTrim: { },
type: Boolean, inputEmptyIsNull: {
default: false type: Boolean,
}, default: false
inputEmptyIsNull: { },
type: Boolean, inputAttrs: {
default: false type: Object,
}, default() {
inputAttrs: { return {};
type: Object,
default() {
return {};
}
},
autofocus: {
type: Boolean,
default: false
},
advanced: {
type: Boolean,
default: false
} }
}, },
computed: { autofocus: {
modelValue: { type: Boolean,
get() { default: false
return this.field.$model;
},
set(value) {
if ((this.isNumeric || this.inputEmptyIsNull) && '' === value) {
value = null;
}
this.field.$model = value;
}
},
filteredScopedSlots() {
return _.filter(this.$slots, (slot, name) => {
return !_.includes([
'default', 'label', 'description'
], name);
});
},
fieldState() {
return this.field.$dirty ? !this.field.$error : null;
},
isRequired() {
return _.has(this.field, 'required');
},
isNumeric() {
return this.inputNumber || this.inputType === "number";
}
}, },
methods: { advanced: {
focus() { type: Boolean,
if (typeof this.$refs.input !== "undefined") { default: false
this.$refs.input.focus();
}
}
} }
} });
const modelValue = computed({
get() {
return props.field.$model;
},
set(newValue) {
if ((props.isNumeric || props.inputEmptyIsNull) && '' === newValue) {
newValue = null;
}
props.field.$model = newValue;
}
});
const filteredSlots = useSlotsExcept(['default', 'label', 'description']);
const fieldState = computed(() => {
return props.field.$dirty ? !props.field.$error : null;
});
const isRequired = computed(() => {
return has(props.field, 'required');
});
const isNumeric = computed(() => {
return props.inputNumber || props.inputType === "number";
});
const input = ref(); // Input
const focus = () => {
input.value?.focus();
};
defineExpose({
focus
});
</script> </script>

View File

@ -6,76 +6,79 @@
</div> </div>
</template> </template>
<script> <script setup>
import _ from 'lodash'; import {useTranslate} from "~/vendor/gettext";
import {get, map} from "lodash";
import {computed} from "vue";
export default { const props = defineProps({
name: 'VuelidateError', field: Object
props: { });
field: Object
const {$gettext} = useTranslate();
const messages = {
required: () => {
return $gettext('This field is required.');
}, },
data() { minLength: (params) => {
return { return $gettext(
messages: { 'This field must have at least %{ min } letters.',
required: () => { params
return this.$gettext('This field is required.'); );
}, },
minLength: (params) => { maxLength: (params) => {
let text = this.$gettext('This field must have at least %{ min } letters.'); return $gettext(
return this.$gettextInterpolate(text, params); 'This field must have at most %{ max } letters.',
}, params
maxLength: (params) => { );
let text = this.$gettext('This field must have at most %{ max } letters.'); },
return this.$gettextInterpolate(text, params); between: (params) => {
}, return $gettext(
between: (params) => { 'This field must be between %{ min } and %{ max }.',
let text = this.$gettext('This field must be between %{ min } and %{ max }.'); params
return this.$gettextInterpolate(text, params); );
}, },
alpha: () => { alpha: () => {
return this.$gettext('This field must only contain alphabetic characters.'); return $gettext('This field must only contain alphabetic characters.');
}, },
alphaNum: () => { alphaNum: () => {
return this.$gettext('This field must only contain alphanumeric characters.'); return $gettext('This field must only contain alphanumeric characters.');
}, },
numeric: () => { numeric: () => {
return this.$gettext('This field must only contain numeric characters.'); return $gettext('This field must only contain numeric characters.');
}, },
integer: () => { integer: () => {
return this.$gettext('This field must be a valid integer.'); return $gettext('This field must be a valid integer.');
}, },
decimal: () => { decimal: () => {
return this.$gettext('This field must be a valid decimal number.'); return $gettext('This field must be a valid decimal number.');
}, },
email: () => { email: () => {
return this.$gettext('This field must be a valid e-mail address.'); return $gettext('This field must be a valid e-mail address.');
}, },
ipAddress: () => { ipAddress: () => {
return this.$gettext('This field must be a valid IP address.'); return $gettext('This field must be a valid IP address.');
}, },
url: () => { url: () => {
return this.$gettext('This field must be a valid URL.'); return $gettext('This field must be a valid URL.');
}, },
validatePassword: () => { validatePassword: () => {
return this.$gettext('This password is too common or insecure.'); return $gettext('This password is too common or insecure.');
} }
};
const errorMessages = computed(() => {
return map(
props.field.$errors,
(error) => {
const message = get(messages, error.$validator, null);
if (null !== message) {
return message(error.$params);
} else {
return error.$message;
} }
} }
}, );
computed: { });
errorMessages() {
let errors = [];
_.forEach(this.field.$errors, (error) => {
const message = _.get(this.messages, error.$validator, null);
if (null !== message) {
errors.push(message(error.$params));
} else {
errors.push(error.$message);
}
});
return errors;
}
}
}
</script> </script>

View File

@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<song-history-modal :show-album-art="showAlbumArt" ref="history_modal"></song-history-modal> <song-history-modal :show-album-art="showAlbumArt" :history="history"></song-history-modal>
<request-modal :show-album-art="showAlbumArt" :request-list-uri="requestListUri" <request-modal :show-album-art="showAlbumArt" :request-list-uri="requestListUri"
:custom-fields="customFields"></request-modal> :custom-fields="customFields"></request-modal>
</template> </template>
@ -64,12 +64,9 @@ const props = defineProps({
} }
}); });
const history_modal = ref(); // Template ref const history = ref({});
const isMounted = useMounted();
const onNowPlayingUpdate = (newNowPlaying) => { const onNowPlayingUpdate = (newNowPlaying) => {
if (isMounted.value) { history.value = newNowPlaying?.song_history;
history_modal.value.updateHistory(newNowPlaying);
}
} }
</script> </script>

View File

@ -1,44 +1,33 @@
<template> <template>
<b-modal size="lg" id="request_modal" ref="modal" :title="langTitle" hide-footer> <b-modal size="lg" id="request_modal" ref="modal" :title="$gettext('Request a Song')" hide-footer>
<song-request :show-album-art="showAlbumArt" :request-list-uri="requestListUri" :custom-fields="customFields" <song-request :show-album-art="showAlbumArt" :request-list-uri="requestListUri" :custom-fields="customFields"
@submitted="doClose"></song-request> @submitted="doClose"></song-request>
</b-modal> </b-modal>
</template> </template>
<script> <script setup>
import SongRequest from '../Requests'; import SongRequest from '../Requests';
import {ref} from "vue";
export default { const props = defineProps({
components: { SongRequest }, requestListUri: {
props: { type: String,
requestListUri: { required: true
type: String,
required: true
},
showAlbumArt: {
type: Boolean,
default: true
},
customFields: {
type: Array,
required: false,
default: () => []
}
}, },
data () { showAlbumArt: {
return { type: Boolean,
loading: true default: true
};
}, },
computed: { customFields: {
langTitle () { type: Array,
return this.$gettext('Request a Song'); required: false,
} default: () => []
},
methods: {
doClose () {
this.$refs.modal.hide();
}
} }
});
const modal = ref(); // BModal
const doClose = () => {
modal.value?.hide();
}; };
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="station-history"> <div id="station-history">
<p v-if="history.length <= 0">{{ langNoRecords }}</p> <p v-if="history.length <= 0">{{ $gettext('No records to display.') }}</p>
<div class="song" v-for="(row, index) in history"> <div class="song" v-for="(row, index) in history">
<strong class="order">{{ history.length - index }}</strong> <strong class="order">{{ history.length - index }}</strong>
<img v-if="showAlbumArt" class="art" :src="row.song.art"> <img v-if="showAlbumArt" class="art" :src="row.song.art">
@ -77,33 +77,26 @@
} }
</style> </style>
<script> <script setup>
import {DateTime} from 'luxon'; import {DateTime} from "luxon";
export default { const props = defineProps({
props: { history: Array,
history: Array, showAlbumArt: {
showAlbumArt: { type: Boolean,
type: Boolean, default: true
default: true
},
}, },
computed: { });
langNoRecords () {
return this.$gettext('No records to display.');
}
},
methods: {
unixTimestampToDate (timestamp) {
if (!timestamp) {
return '';
}
return DateTime.fromSeconds(timestamp).toRelative(); const unixTimestampToDate = (timestamp) => {
}, if (!timestamp) {
albumAndArtist (song) { return '';
return [song.artist, song.album].filter(str => !!str).join(', ');
}
} }
return DateTime.fromSeconds(timestamp).toRelative();
};
const albumAndArtist = (song) => {
return [song.artist, song.album].filter(str => !!str).join(', ');
}; };
</script> </script>

View File

@ -1,34 +1,17 @@
<template> <template>
<b-modal size="md" id="song_history_modal" ref="modal" :title="langTitle" centered hide-footer> <b-modal size="md" id="song_history_modal" ref="modal" :title="$gettext('Song History')" centered hide-footer>
<song-history :show-album-art="showAlbumArt" :history="history"></song-history> <song-history :show-album-art="showAlbumArt" :history="history"></song-history>
</b-modal> </b-modal>
</template> </template>
<script> <script setup>
import SongHistory from './SongHistory'; import SongHistory from './SongHistory';
export default { const props = defineProps({
components: {SongHistory}, history: Array,
props: { showAlbumArt: {
showAlbumArt: { type: Boolean,
type: Boolean, default: true
default: true
},
}, },
data() { });
return {
history: []
};
},
computed: {
langTitle() {
return this.$gettext('Song History');
}
},
methods: {
updateHistory (np) {
this.history = np.song_history;
}
}
};
</script> </script>

View File

@ -27,7 +27,7 @@
:is-stream="false"></play-button> :is-stream="false"></play-button>
<template v-if="showDownloadButton"> <template v-if="showDownloadButton">
&nbsp; &nbsp;
<a class="name" :href="row.item.download_url" target="_blank" :title="langDownload"> <a class="name" :href="row.item.download_url" target="_blank" :title="$gettext('Download')">
<icon icon="cloud_download"></icon> <icon icon="cloud_download"></icon>
</a> </a>
</template> </template>
@ -35,7 +35,7 @@
<template #cell(media_art)="row"> <template #cell(media_art)="row">
<a :href="row.item.media_art" class="album-art" target="_blank" <a :href="row.item.media_art" class="album-art" target="_blank"
data-fancybox="gallery"> data-fancybox="gallery">
<img class="media_manager_album_art" :alt="langAlbumArt" :src="row.item.media_art"> <img class="media_manager_album_art" :alt="$gettext('Album Art')" :src="row.item.media_art">
</a> </a>
</template> </template>
<template #cell(size)="row"> <template #cell(size)="row">
@ -90,59 +90,41 @@
border-radius: 5px; border-radius: 5px;
} }
} }
</style> </style>
<script> <script setup>
import InlinePlayer from '../InlinePlayer'; import InlinePlayer from '../InlinePlayer';
import DataTable from '~/components/Common/DataTable'; import DataTable from '~/components/Common/DataTable';
import _ from 'lodash'; import {forEach} from 'lodash';
import Icon from '~/components/Common/Icon'; import Icon from '~/components/Common/Icon';
import PlayButton from "~/components/Common/PlayButton"; import PlayButton from "~/components/Common/PlayButton";
import {useTranslate} from "~/vendor/gettext";
export default { const props = defineProps({
components: {PlayButton, Icon, DataTable, InlinePlayer}, listUrl: String,
props: { stationName: String,
listUrl: String, customFields: Array,
stationName: String, showDownloadButton: Boolean
customFields: Array, });
showDownloadButton: Boolean
},
data() {
let fields = [
{key: 'download_url', label: ' '},
{key: 'media_art', label: this.$gettext('Art')},
{key: 'media_title', label: this.$gettext('Title'), sortable: true, selectable: true},
{key: 'media_artist', label: this.$gettext('Artist'), sortable: true, selectable: true},
{key: 'media_album', label: this.$gettext('Album'), sortable: true, selectable: true, visible: false},
{key: 'playlist', label: this.$gettext('Playlist'), sortable: true, selectable: true, visible: false}
];
_.forEach(this.customFields.slice(), (field) => { const {$gettext} = useTranslate();
fields.push({
key: field.display_key,
label: field.label,
sortable: true,
selectable: true,
visible: false
});
});
return { let fields = [
fields: fields {key: 'download_url', label: ' '},
}; {key: 'media_art', label: $gettext('Art')},
}, {key: 'media_title', label: $gettext('Title'), sortable: true, selectable: true},
computed: { {key: 'media_artist', label: $gettext('Artist'), sortable: true, selectable: true},
langAlbumArt () { {key: 'media_album', label: $gettext('Album'), sortable: true, selectable: true, visible: false},
return this.$gettext('Album Art'); {key: 'playlist', label: $gettext('Playlist'), sortable: true, selectable: true, visible: false}
}, ];
langPlayPause () {
return this.$gettext('Play/Pause'); forEach(props.customFields.slice(), (field) => {
}, fields.push({
langDownload () { key: field.display_key,
return this.$gettext('Download'); label: field.label,
} sortable: true,
} selectable: true,
}; visible: false
});
});
</script> </script>

View File

@ -39,15 +39,12 @@
} }
</style> </style>
<script> <script setup>
import Schedule from '~/components/Common/ScheduleView'; import Schedule from '~/components/Common/ScheduleView';
export default { const props = defineProps({
components: { Schedule }, scheduleUrl: String,
props: { stationName: String,
scheduleUrl: String, stationTimeZone: String
stationName: String, });
stationTimeZone: String
},
};
</script> </script>

View File

@ -1,25 +0,0 @@
<template>
</template>
<script>
export default {
name: 'StationMayNeedRestart',
props: {
restartStatusUrl: String
},
methods: {
mayNeedRestart() {
this.axios.get(this.restartStatusUrl).then((resp) => {
if (resp.data.needs_restart) {
this.needsRestart();
}
});
},
needsRestart() {
document.dispatchEvent(new CustomEvent("station-needs-restart"));
}
}
}
;
</script>

View File

@ -0,0 +1,35 @@
import {useAxios} from "~/vendor/axios";
export const mayNeedRestartProps = {
restartStatusUrl: String
};
export function useNeedsRestart() {
const needsRestart = () => {
document.dispatchEvent(new CustomEvent("station-needs-restart"));
}
return {
needsRestart
};
}
export function useMayNeedRestart(restartStatusUrl) {
const {needsRestart} = useNeedsRestart();
const {axios} = useAxios();
const mayNeedRestart = () => {
axios.get(restartStatusUrl).then((resp) => {
if (resp.data.needs_restart) {
needsRestart();
}
});
}
return {
needsRestart,
mayNeedRestart
}
}

View File

@ -43,65 +43,77 @@
</data-table> </data-table>
</b-card> </b-card>
<edit-modal ref="editModal" :create-url="listUrl" @relist="relist" @needs-restart="mayNeedRestart"></edit-modal> <edit-modal ref="editmodal" :create-url="listUrl" @relist="relist" @needs-restart="mayNeedRestart"></edit-modal>
</template> </template>
<script> <script setup>
import DataTable from '~/components/Common/DataTable'; import DataTable from '~/components/Common/DataTable';
import EditModal from './HlsStreams/EditModal'; import EditModal from './HlsStreams/EditModal';
import Icon from '~/components/Common/Icon'; import Icon from '~/components/Common/Icon';
import InfoCard from '~/components/Common/InfoCard'; import InfoCard from '~/components/Common/InfoCard';
import StationMayNeedRestart from '~/components/Stations/Common/MayNeedRestart.vue'; import {useTranslate} from "~/vendor/gettext";
import {ref} from "vue";
import {mayNeedRestartProps, useMayNeedRestart} from "~/components/Stations/Common/useMayNeedRestart";
import {useSweetAlert} from "~/vendor/sweetalert";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default { const props = defineProps({
name: 'StationHlsStreams', ...mayNeedRestartProps,
components: {InfoCard, Icon, EditModal, DataTable}, listUrl: String
mixins: [StationMayNeedRestart], });
props: {
listUrl: String const {$gettext} = useTranslate();
},
data() { const fields = [
return { {key: 'name', isRowHeader: true, label: $gettext('Name'), sortable: true},
fields: [ {key: 'format', label: $gettext('Format'), sortable: true},
{key: 'name', isRowHeader: true, label: this.$gettext('Name'), sortable: true}, {key: 'bitrate', label: $gettext('Bitrate'), sortable: true},
{key: 'format', label: this.$gettext('Format'), sortable: true}, {key: 'actions', label: $gettext('Actions'), sortable: false, class: 'shrink'}
{key: 'bitrate', label: this.$gettext('Bitrate'), sortable: true}, ];
{key: 'actions', label: this.$gettext('Actions'), sortable: false, class: 'shrink'}
] const upper = (data) => {
}; let upper = [];
}, data.split(' ').forEach((word) => {
methods: { upper.push(word.toUpperCase());
upper(data) { });
let upper = []; return upper.join(' ');
data.split(' ').forEach((word) => { };
upper.push(word.toUpperCase());
const datatable = ref(); // DataTable
const relist = () => {
datatable.value?.refresh();
};
const editmodal = ref(); // EditModal
const doCreate = () => {
editmodal.value?.create();
};
const doEdit = (url) => {
editmodal.value?.edit(url);
};
const {mayNeedRestart, needsRestart} = useMayNeedRestart(props.restartStatusUrl);
const {confirmDelete} = useSweetAlert();
const {wrapWithLoading, notifySuccess} = useNotify();
const {axios} = useAxios();
const doDelete = (url) => {
confirmDelete({
title: $gettext('Delete HLS Stream?'),
}).then((result) => {
if (result.value) {
wrapWithLoading(
axios.delete(url)
).then((resp) => {
notifySuccess(resp.data.message);
needsRestart();
relist();
}); });
return upper.join(' '); }
}, });
relist() {
this.$refs.datatable.refresh();
},
doCreate() {
this.$refs.editModal.create();
},
doEdit(url) {
this.$refs.editModal.edit(url);
},
doDelete(url) {
this.$confirmDelete({
title: this.$gettext('Delete HLS Stream?'),
}).then((result) => {
if (result.value) {
this.$wrapWithLoading(
this.axios.delete(url)
).then((resp) => {
this.$notifySuccess(resp.data.message);
this.needsRestart();
this.relist();
});
}
});
},
}
}; };
</script> </script>

View File

@ -28,7 +28,7 @@
<b-overlay variant="card" :show="loading"> <b-overlay variant="card" :show="loading">
<div class="card-body"> <div class="card-body">
<b-form-fieldset v-for="(row, index) in config" :key="index" class="mb-0"> <b-form-fieldset v-for="(row, index) in config" :key="index" class="mb-0">
<b-wrapped-form-group v-if="row.is_field" :field="v$.form[row.field_name]" <b-wrapped-form-group v-if="row.is_field" :field="v$[row.field_name]"
:id="'form_edit_'+row.field_name" input-type="textarea" :id="'form_edit_'+row.field_name" input-type="textarea"
:input-attrs="{class: 'text-preformatted mb-3', spellcheck: 'false', 'max-rows': 20, rows: 5}"> :input-attrs="{class: 'text-preformatted mb-3', spellcheck: 'false', 'max-rows': 20, rows: 5}">
</b-wrapped-form-group> </b-wrapped-form-group>
@ -37,7 +37,7 @@
</b-form-markup> </b-form-markup>
</b-form-fieldset> </b-form-fieldset>
<b-button size="lg" type="submit" :variant="(v$.form.$invalid) ? 'danger' : 'primary'"> <b-button size="lg" type="submit" :variant="(v$.$invalid) ? 'danger' : 'primary'">
{{ $gettext('Save Changes') }} {{ $gettext('Save Changes') }}
</b-button> </b-button>
</div> </div>
@ -46,83 +46,78 @@
</form> </form>
</template> </template>
<script> <script setup>
import useVuelidate from "@vuelidate/core";
import BFormFieldset from "~/components/Form/BFormFieldset"; import BFormFieldset from "~/components/Form/BFormFieldset";
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup"; import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
import BFormMarkup from "~/components/Form/BFormMarkup"; import BFormMarkup from "~/components/Form/BFormMarkup";
import _ from "lodash"; import {forEach} from "lodash";
import mergeExisting from "~/functions/mergeExisting"; import mergeExisting from "~/functions/mergeExisting";
import InfoCard from "~/components/Common/InfoCard"; import InfoCard from "~/components/Common/InfoCard";
import StationMayNeedRestart from '~/components/Stations/Common/MayNeedRestart.vue'; import {useVuelidateOnForm} from "~/components/Form/UseVuelidateOnForm";
import {onMounted, ref} from "vue";
import {mayNeedRestartProps, useMayNeedRestart} from "~/components/Stations/Common/useMayNeedRestart";
import {useAxios} from "~/vendor/axios";
import {useNotify} from "~/vendor/bootstrapVue";
export default { const props = defineProps({
name: 'StationsLiquidsoapConfig', ...mayNeedRestartProps,
components: {InfoCard, BFormFieldset, BWrappedFormGroup, BFormMarkup}, settingsUrl: String,
setup() { config: Array,
return {v$: useVuelidate()} sections: Array,
}, });
props: {
settingsUrl: String,
config: Array,
sections: Array,
},
mixins: [
StationMayNeedRestart,
],
validations() {
let validations = {form: {}};
_.forEach(this.sections, (section) => {
validations.form[section] = {};
});
return validations;
},
data() {
return {
loading: true,
form: {},
}
},
mounted() {
this.relist();
},
methods: {
resetForm() {
let form = {};
_.forEach(this.sections, (section) => {
form[section] = null;
});
this.form = form;
},
relist() {
this.resetForm();
this.v$.$reset();
this.loading = true; const buildForm = () => {
this.axios.get(this.settingsUrl).then((resp) => { let validations = {};
this.form = mergeExisting(this.form, resp.data); let blankForm = {};
this.loading = false;
});
},
submit() {
this.v$.$touch();
if (this.v$.$errors.length > 0) {
return;
}
this.$wrapWithLoading( forEach(props.sections, (section) => {
this.axios({ validations[section] = {};
method: 'PUT', blankForm[section] = null;
url: this.settingsUrl, });
data: this.form
})
).then(() => {
this.$notifySuccess();
this.mayNeedRestart(); return {validations, blankForm};
this.relist(); }
});
} const {validations, blankForm} = buildForm();
const {form, resetForm, v$} = useVuelidateOnForm(validations, blankForm);
const loading = ref();
const {mayNeedRestart} = useMayNeedRestart(props.restartStatusUrl);
const {axios} = useAxios();
const relist = () => {
resetForm();
loading.value = true;
axios.get(props.settingsUrl).then((resp) => {
form.value = mergeExisting(form.value, resp.data);
loading.value = false;
});
};
onMounted(relist);
const {wrapWithLoading, notifySuccess} = useNotify();
const submit = () => {
v$.value.$touch();
if (v$.value.$errors.length > 0) {
return;
} }
wrapWithLoading(
axios({
method: 'PUT',
url: props.settingsUrl,
data: form.value,
})
).then(() => {
notifySuccess();
mayNeedRestart();
relist();
});
} }
</script> </script>

View File

@ -54,72 +54,87 @@
</data-table> </data-table>
</b-card> </b-card>
<edit-modal ref="editModal" :create-url="listUrl" :new-intro-url="newIntroUrl" <edit-modal ref="editmodal" :create-url="listUrl" :new-intro-url="newIntroUrl"
:show-advanced="showAdvanced" :station-frontend-type="stationFrontendType" :show-advanced="showAdvanced" :station-frontend-type="stationFrontendType"
@relist="relist" @needs-restart="mayNeedRestart"></edit-modal> @relist="relist" @needs-restart="mayNeedRestart"></edit-modal>
</template> </template>
<script> <script setup>
import DataTable from '~/components/Common/DataTable'; import DataTable from '~/components/Common/DataTable';
import EditModal from './Mounts/EditModal'; import EditModal from './Mounts/EditModal';
import Icon from '~/components/Common/Icon'; import Icon from '~/components/Common/Icon';
import InfoCard from '~/components/Common/InfoCard'; import InfoCard from '~/components/Common/InfoCard';
import StationMayNeedRestart from '~/components/Stations/Common/MayNeedRestart.vue'; import {mayNeedRestartProps, useMayNeedRestart} from "~/components/Stations/Common/useMayNeedRestart";
import {useTranslate} from "~/vendor/gettext";
import {ref} from "vue";
import {useSweetAlert} from "~/vendor/sweetalert";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default { const props = defineProps({
name: 'StationMounts', ...mayNeedRestartProps,
components: {InfoCard, Icon, EditModal, DataTable}, listUrl: String,
mixins: [StationMayNeedRestart], newIntroUrl: String,
props: { stationFrontendType: String,
listUrl: String, showAdvanced: {
newIntroUrl: String, type: Boolean,
stationFrontendType: String, default: true
showAdvanced: {
type: Boolean,
default: true
},
}, },
data() { });
return {
fields: [ const {$gettext} = useTranslate();
{key: 'display_name', isRowHeader: true, label: this.$gettext('Name'), sortable: true},
{key: 'enable_autodj', label: this.$gettext('AutoDJ'), sortable: true}, const fields = [
{key: 'actions', label: this.$gettext('Actions'), sortable: false, class: 'shrink'} {key: 'display_name', isRowHeader: true, label: $gettext('Name'), sortable: true},
] {key: 'enable_autodj', label: $gettext('AutoDJ'), sortable: true},
}; {key: 'actions', label: $gettext('Actions'), sortable: false, class: 'shrink'}
}, ];
methods: {
upper(data) { const upper = (data) => {
let upper = []; let upper = [];
data.split(' ').forEach((word) => { data.split(' ').forEach((word) => {
upper.push(word.toUpperCase()); upper.push(word.toUpperCase());
}); });
return upper.join(' '); return upper.join(' ');
}, };
relist() {
this.$refs.datatable.refresh(); const datatable = ref(); // DataTable
},
doCreate() { const relist = () => {
this.$refs.editModal.create(); datatable.value.refresh();
}, };
doEdit(url) {
this.$refs.editModal.edit(url); const editmodal = ref(); // EditModal
},
doDelete(url) { const doCreate = () => {
this.$confirmDelete({ editmodal.value.create();
title: this.$gettext('Delete Mount Point?'), };
}).then((result) => {
if (result.value) { const doEdit = (url) => {
this.$wrapWithLoading( editmodal.value.edit(url);
this.axios.delete(url) };
).then((resp) => {
this.$notifySuccess(resp.data.message); const {needsRestart, mayNeedRestart} = useMayNeedRestart(props.restartStatusUrl);
this.needsRestart();
this.relist(); const {confirmDelete} = useSweetAlert();
}); const {wrapWithLoading, notifySuccess} = useNotify();
} const {axios} = useAxios();
const doDelete = (url) => {
confirmDelete({
title: $gettext('Delete Mount Point?'),
}).then((result) => {
if (result.value) {
wrapWithLoading(
axios.delete(url)
).then((resp) => {
notifySuccess(resp.data.message);
needsRestart();
relist();
}); });
} }
} });
}; }
</script> </script>

View File

@ -48,71 +48,83 @@
</data-table> </data-table>
</b-card> </b-card>
<remote-edit-modal ref="editModal" :create-url="listUrl" <remote-edit-modal ref="editmodal" :create-url="listUrl"
@relist="relist" @needs-restart="mayNeedRestart"></remote-edit-modal> @relist="relist" @needs-restart="mayNeedRestart"></remote-edit-modal>
</template> </template>
<script> <script setup>
import DataTable from '~/components/Common/DataTable'; import DataTable from '~/components/Common/DataTable';
import EditModal from './Mounts/EditModal';
import Icon from '~/components/Common/Icon'; import Icon from '~/components/Common/Icon';
import InfoCard from '~/components/Common/InfoCard'; import InfoCard from '~/components/Common/InfoCard';
import RemoteEditModal from "./Remotes/EditModal"; import RemoteEditModal from "./Remotes/EditModal";
import StationMayNeedRestart from '~/components/Stations/Common/MayNeedRestart.vue';
import '~/vendor/sweetalert'; import '~/vendor/sweetalert';
import {mayNeedRestartProps, useMayNeedRestart} from "~/components/Stations/Common/useMayNeedRestart";
import {useTranslate} from "~/vendor/gettext";
import {ref} from "vue";
import {useSweetAlert} from "~/vendor/sweetalert";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default { const props = defineProps({
name: 'StationMounts', ...mayNeedRestartProps,
components: {RemoteEditModal, InfoCard, Icon, EditModal, DataTable}, listUrl: String,
mixins: [StationMayNeedRestart], });
props: {
listUrl: String,
},
data() {
return {
fields: [
{key: 'display_name', isRowHeader: true, label: this.$gettext('Name'), sortable: true},
{key: 'enable_autodj', label: this.$gettext('AutoDJ'), sortable: true},
{key: 'actions', label: this.$gettext('Actions'), sortable: false, class: 'shrink'}
]
};
},
methods: {
upper(data) {
if (!data) {
return '';
}
let upper = []; const {$gettext} = useTranslate();
data.split(' ').forEach((word) => {
upper.push(word.toUpperCase()); const fields = [
}); {key: 'display_name', isRowHeader: true, label: $gettext('Name'), sortable: true},
return upper.join(' '); {key: 'enable_autodj', label: $gettext('AutoDJ'), sortable: true},
}, {key: 'actions', label: $gettext('Actions'), sortable: false, class: 'shrink'}
relist() { ];
this.$refs.datatable.refresh();
}, const upper = (data) => {
doCreate() { if (!data) {
this.$refs.editModal.create(); return '';
},
doEdit(url) {
this.$refs.editModal.edit(url);
},
doDelete(url) {
this.$confirmDelete({
title: this.$gettext('Delete Remote Relay?'),
}).then((result) => {
if (result.value) {
this.$wrapWithLoading(
this.axios.delete(url)
).then((resp) => {
this.$notifySuccess(resp.data.message);
this.needsRestart();
this.relist();
});
}
});
},
} }
let upper = [];
data.split(' ').forEach((word) => {
upper.push(word.toUpperCase());
});
return upper.join(' ');
};
const datatable = ref(); // DataTable
const relist = () => {
datatable.value?.refresh();
};
const editmodal = ref(); // EditModal
const doCreate = () => {
editmodal.value?.create();
};
const doEdit = (url) => {
editmodal.value?.edit(url);
};
const {mayNeedRestart, needsRestart} = useMayNeedRestart(props.restartStatusUrl);
const {confirmDelete} = useSweetAlert();
const {wrapWithLoading, notifySuccess} = useNotify();
const {axios} = useAxios();
const doDelete = (url) => {
confirmDelete({
title: $gettext('Delete Remote Relay?'),
}).then((result) => {
if (result.value) {
wrapWithLoading(
axios.delete(url)
).then((resp) => {
notifySuccess(resp.data.message);
needsRestart();
relist();
});
}
});
}; };
</script> </script>

View File

@ -28,7 +28,7 @@
}} }}
</template> </template>
<flow-upload :target-url="apiUrl" :valid-mime-types="acceptMimeTypes" <flow-upload :target-url="apiUrl" :valid-mime-types="['text/plain']"
@success="onFileSuccess"></flow-upload> @success="onFileSuccess"></flow-upload>
</b-form-group> </b-form-group>
@ -56,42 +56,42 @@
</section> </section>
</template> </template>
<script> <script setup>
import FlowUpload from '~/components/Common/FlowUpload'; import FlowUpload from '~/components/Common/FlowUpload';
import InfoCard from "~/components/Common/InfoCard"; import InfoCard from "~/components/Common/InfoCard";
import StationMayNeedRestart from '~/components/Stations/Common/MayNeedRestart.vue'; import {ref} from "vue";
import {mayNeedRestartProps, useMayNeedRestart} from "~/components/Stations/Common/useMayNeedRestart";
import {useNotify} from "~/vendor/bootstrapVue";
import {useAxios} from "~/vendor/axios";
export default { const props = defineProps({
name: 'StationsStereoToolConfiguration', ...mayNeedRestartProps,
components: {InfoCard, FlowUpload}, recordHasStereoToolConfiguration: Boolean,
mixins: [StationMayNeedRestart], apiUrl: String
props: { });
recordHasStereoToolConfiguration: Boolean,
apiUrl: String const hasStereoToolConfiguration = ref(props.recordHasStereoToolConfiguration);
},
data() { const {mayNeedRestart} = useMayNeedRestart(props.restartStatusUrl);
return {
hasStereoToolConfiguration: this.recordHasStereoToolConfiguration, const onFileSuccess = () => {
acceptMimeTypes: ['text/plain'] mayNeedRestart();
}; hasStereoToolConfiguration.value = true;
}, };
methods: {
onFileSuccess() { const {wrapWithLoading, notifySuccess} = useNotify();
this.mayNeedRestart(); const {axios} = useAxios();
this.hasStereoToolConfiguration = true;
}, const deleteConfigurationFile = () => {
deleteConfigurationFile() { wrapWithLoading(
this.$wrapWithLoading( axios({
this.axios({ method: 'DELETE',
method: 'DELETE', url: this.apiUrl
url: this.apiUrl })
}) ).then(() => {
).then(() => { mayNeedRestart();
this.mayNeedRestart(); hasStereoToolConfiguration.value = false;
this.hasStereoToolConfiguration = false; notifySuccess();
this.$notifySuccess(); });
});
},
}
}; };
</script> </script>

View File

@ -0,0 +1,12 @@
import {filter, includes} from "lodash";
import {computed, useSlots} from "vue";
export default function useSlotsExcept(except) {
const slots = useSlots();
return computed(() => {
return filter(slots, (slot, name) => {
return !includes(except, name);
});
});
};