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>
</template>
<script>
<script setup>
import LogList from "~/components/Common/LogList";
import StreamingLogModal from "~/components/Common/StreamingLogModal";
import {ref} from "vue";
export default {
name: 'AdminLogs',
components: {StreamingLogModal, LogList},
props: {
systemLogsUrl: String,
stationLogs: Array
},
methods: {
viewLog(url) {
this.$refs.modal.show(url);
}
}
}
const props = defineProps({
systemLogsUrl: String,
stationLogs: Array
});
const modal = ref(); // StreamingLogModal
const viewLog = (url) => {
modal.value.show(url);
};
</script>

View File

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

View File

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

View File

@ -13,34 +13,21 @@
<slot name="description" v-bind="slotProps"></slot>
</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>
</template>
</b-form-group>
</template>
<script>
import _ from "lodash";
<script setup>
import useSlotsExcept from "~/functions/useSlotsExcept";
export default {
name: 'BFormMarkup',
props: {
id: {
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 props = defineProps({
id: {
type: String,
required: true
}
}
});
const filteredSlots = useSlotsExcept(['default', 'label', 'description']);
</script>

View File

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

View File

@ -29,98 +29,96 @@
<slot v-bind="slotProps" name="description"></slot>
</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>
</template>
</b-form-group>
</template>
<script>
import _ from "lodash";
<script setup>
import VuelidateError from "./VuelidateError";
import {computed, ref} from "vue";
import useSlotsExcept from "~/functions/useSlotsExcept";
import {has} from "lodash";
export default {
name: 'BWrappedFormGroup',
components: {VuelidateError},
props: {
id: {
type: String,
required: true
},
name: {
type: String,
},
field: {
type: Object,
required: true
},
inputType: {
type: String,
default: 'text'
},
inputNumber: {
type: Boolean,
default: false
},
inputTrim: {
type: Boolean,
default: false
},
inputEmptyIsNull: {
type: Boolean,
default: false
},
inputAttrs: {
type: Object,
default() {
return {};
}
},
autofocus: {
type: Boolean,
default: false
},
advanced: {
type: Boolean,
default: false
const props = defineProps({
id: {
type: String,
required: true
},
name: {
type: String,
},
field: {
type: Object,
required: true
},
inputType: {
type: String,
default: 'text'
},
inputNumber: {
type: Boolean,
default: false
},
inputTrim: {
type: Boolean,
default: false
},
inputEmptyIsNull: {
type: Boolean,
default: false
},
inputAttrs: {
type: Object,
default() {
return {};
}
},
computed: {
modelValue: {
get() {
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";
}
autofocus: {
type: Boolean,
default: false
},
methods: {
focus() {
if (typeof this.$refs.input !== "undefined") {
this.$refs.input.focus();
}
}
advanced: {
type: Boolean,
default: false
}
}
});
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>

View File

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

View File

@ -26,7 +26,7 @@
</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"
:custom-fields="customFields"></request-modal>
</template>
@ -64,12 +64,9 @@ const props = defineProps({
}
});
const history_modal = ref(); // Template ref
const isMounted = useMounted();
const history = ref({});
const onNowPlayingUpdate = (newNowPlaying) => {
if (isMounted.value) {
history_modal.value.updateHistory(newNowPlaying);
}
history.value = newNowPlaying?.song_history;
}
</script>

View File

@ -1,44 +1,33 @@
<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"
@submitted="doClose"></song-request>
</b-modal>
</template>
<script>
<script setup>
import SongRequest from '../Requests';
import {ref} from "vue";
export default {
components: { SongRequest },
props: {
requestListUri: {
type: String,
required: true
},
showAlbumArt: {
type: Boolean,
default: true
},
customFields: {
type: Array,
required: false,
default: () => []
}
const props = defineProps({
requestListUri: {
type: String,
required: true
},
data () {
return {
loading: true
};
showAlbumArt: {
type: Boolean,
default: true
},
computed: {
langTitle () {
return this.$gettext('Request a Song');
}
},
methods: {
doClose () {
this.$refs.modal.hide();
}
customFields: {
type: Array,
required: false,
default: () => []
}
});
const modal = ref(); // BModal
const doClose = () => {
modal.value?.hide();
};
</script>

View File

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

View File

@ -1,34 +1,17 @@
<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>
</b-modal>
</template>
<script>
<script setup>
import SongHistory from './SongHistory';
export default {
components: {SongHistory},
props: {
showAlbumArt: {
type: Boolean,
default: true
},
const props = defineProps({
history: Array,
showAlbumArt: {
type: Boolean,
default: true
},
data() {
return {
history: []
};
},
computed: {
langTitle() {
return this.$gettext('Song History');
}
},
methods: {
updateHistory (np) {
this.history = np.song_history;
}
}
};
});
</script>

View File

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

View File

@ -39,15 +39,12 @@
}
</style>
<script>
<script setup>
import Schedule from '~/components/Common/ScheduleView';
export default {
components: { Schedule },
props: {
scheduleUrl: String,
stationName: String,
stationTimeZone: String
},
};
const props = defineProps({
scheduleUrl: String,
stationName: String,
stationTimeZone: String
});
</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>
</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>
<script>
<script setup>
import DataTable from '~/components/Common/DataTable';
import EditModal from './HlsStreams/EditModal';
import Icon from '~/components/Common/Icon';
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 {
name: 'StationHlsStreams',
components: {InfoCard, Icon, EditModal, DataTable},
mixins: [StationMayNeedRestart],
props: {
listUrl: String
},
data() {
return {
fields: [
{key: 'name', isRowHeader: true, label: this.$gettext('Name'), sortable: true},
{key: 'format', label: this.$gettext('Format'), sortable: true},
{key: 'bitrate', label: this.$gettext('Bitrate'), sortable: true},
{key: 'actions', label: this.$gettext('Actions'), sortable: false, class: 'shrink'}
]
};
},
methods: {
upper(data) {
let upper = [];
data.split(' ').forEach((word) => {
upper.push(word.toUpperCase());
const props = defineProps({
...mayNeedRestartProps,
listUrl: String
});
const {$gettext} = useTranslate();
const fields = [
{key: 'name', isRowHeader: true, label: $gettext('Name'), sortable: true},
{key: 'format', label: $gettext('Format'), sortable: true},
{key: 'bitrate', label: $gettext('Bitrate'), sortable: true},
{key: 'actions', label: $gettext('Actions'), sortable: false, class: 'shrink'}
];
const upper = (data) => {
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 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>

View File

@ -28,7 +28,7 @@
<b-overlay variant="card" :show="loading">
<div class="card-body">
<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"
:input-attrs="{class: 'text-preformatted mb-3', spellcheck: 'false', 'max-rows': 20, rows: 5}">
</b-wrapped-form-group>
@ -37,7 +37,7 @@
</b-form-markup>
</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') }}
</b-button>
</div>
@ -46,83 +46,78 @@
</form>
</template>
<script>
import useVuelidate from "@vuelidate/core";
<script setup>
import BFormFieldset from "~/components/Form/BFormFieldset";
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
import BFormMarkup from "~/components/Form/BFormMarkup";
import _ from "lodash";
import {forEach} from "lodash";
import mergeExisting from "~/functions/mergeExisting";
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 {
name: 'StationsLiquidsoapConfig',
components: {InfoCard, BFormFieldset, BWrappedFormGroup, BFormMarkup},
setup() {
return {v$: useVuelidate()}
},
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();
const props = defineProps({
...mayNeedRestartProps,
settingsUrl: String,
config: Array,
sections: Array,
});
this.loading = true;
this.axios.get(this.settingsUrl).then((resp) => {
this.form = mergeExisting(this.form, resp.data);
this.loading = false;
});
},
submit() {
this.v$.$touch();
if (this.v$.$errors.length > 0) {
return;
}
const buildForm = () => {
let validations = {};
let blankForm = {};
this.$wrapWithLoading(
this.axios({
method: 'PUT',
url: this.settingsUrl,
data: this.form
})
).then(() => {
this.$notifySuccess();
forEach(props.sections, (section) => {
validations[section] = {};
blankForm[section] = null;
});
this.mayNeedRestart();
this.relist();
});
}
return {validations, blankForm};
}
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>

View File

@ -54,72 +54,87 @@
</data-table>
</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"
@relist="relist" @needs-restart="mayNeedRestart"></edit-modal>
</template>
<script>
<script setup>
import DataTable from '~/components/Common/DataTable';
import EditModal from './Mounts/EditModal';
import Icon from '~/components/Common/Icon';
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 {
name: 'StationMounts',
components: {InfoCard, Icon, EditModal, DataTable},
mixins: [StationMayNeedRestart],
props: {
listUrl: String,
newIntroUrl: String,
stationFrontendType: String,
showAdvanced: {
type: Boolean,
default: true
},
const props = defineProps({
...mayNeedRestartProps,
listUrl: String,
newIntroUrl: String,
stationFrontendType: String,
showAdvanced: {
type: Boolean,
default: true
},
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) {
let upper = [];
data.split(' ').forEach((word) => {
upper.push(word.toUpperCase());
});
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 Mount Point?'),
}).then((result) => {
if (result.value) {
this.$wrapWithLoading(
this.axios.delete(url)
).then((resp) => {
this.$notifySuccess(resp.data.message);
this.needsRestart();
this.relist();
});
}
});
const {$gettext} = useTranslate();
const fields = [
{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'}
];
const upper = (data) => {
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 {needsRestart, mayNeedRestart} = useMayNeedRestart(props.restartStatusUrl);
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>

View File

@ -48,71 +48,83 @@
</data-table>
</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>
</template>
<script>
<script setup>
import DataTable from '~/components/Common/DataTable';
import EditModal from './Mounts/EditModal';
import Icon from '~/components/Common/Icon';
import InfoCard from '~/components/Common/InfoCard';
import RemoteEditModal from "./Remotes/EditModal";
import StationMayNeedRestart from '~/components/Stations/Common/MayNeedRestart.vue';
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 {
name: 'StationMounts',
components: {RemoteEditModal, InfoCard, Icon, EditModal, DataTable},
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 '';
}
const props = defineProps({
...mayNeedRestartProps,
listUrl: String,
});
let upper = [];
data.split(' ').forEach((word) => {
upper.push(word.toUpperCase());
});
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 Remote Relay?'),
}).then((result) => {
if (result.value) {
this.$wrapWithLoading(
this.axios.delete(url)
).then((resp) => {
this.$notifySuccess(resp.data.message);
this.needsRestart();
this.relist();
});
}
});
},
const {$gettext} = useTranslate();
const fields = [
{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'}
];
const upper = (data) => {
if (!data) {
return '';
}
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>

View File

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