Vue Composition API updates.

This commit is contained in:
Buster Neece 2023-01-07 20:26:29 -06:00
parent d2c7ff6a82
commit 5d1e46a620
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
5 changed files with 437 additions and 451 deletions

View File

@ -1,183 +1,178 @@
<template>
<audio
v-if="isPlaying"
ref="audio"
ref="$audio"
:title="title"
/>
</template>
<script>
<script setup>
import getLogarithmicVolume from '~/functions/getLogarithmicVolume.js';
import Hls from 'hls.js';
import {usePlayerStore} from "~/store.js";
import {defineComponent} from "vue";
import {nextTick, onMounted, ref, toRef, watch} from "vue";
/* TODO Options API */
export default defineComponent({
props: {
title: {
type: String,
default: null
},
volume: {
type: Number,
default: 55
},
isMuted: {
type: Boolean,
default: false
}
const props = defineProps({
title: {
type: String,
default: null
},
setup() {
return {
store: usePlayerStore()
}
volume: {
type: Number,
default: 55
},
data() {
return {
'audio': null,
'hls': null,
'duration': 0,
'currentTime': 0
};
},
computed: {
isPlaying() {
return this.store.isPlaying;
},
current() {
return this.store.current;
}
},
watch: {
volume(volume) {
if (this.audio !== null) {
this.audio.volume = getLogarithmicVolume(volume);
}
},
isMuted(muted) {
if (this.audio !== null) {
this.audio.muted = muted;
}
},
current(newCurrent) {
let url = newCurrent.url;
if (url === null) {
this.stop();
} else {
this.play();
}
},
},
mounted() {
// Allow pausing from the mobile metadata update.
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('pause', () => {
this.stop();
});
}
},
methods: {
stop() {
if (this.audio !== null) {
this.audio.pause();
this.audio.src = '';
}
if (this.hls !== null) {
this.hls.destroy();
this.hls = null;
}
this.duration = 0;
this.currentTime = 0;
this.store.stopPlaying();
},
play() {
if (this.isPlaying) {
this.stop();
this.$nextTick(() => {
this.play();
});
return;
}
this.store.startPlaying();
this.$nextTick(() => {
this.audio = this.$refs.audio;
// Handle audio errors.
this.audio.onerror = (e) => {
if (e.target.error.code === e.target.error.MEDIA_ERR_NETWORK && this.audio.src !== '') {
console.log('Network interrupted stream. Automatically reconnecting shortly...');
setTimeout(() => {
this.play();
}, 5000);
}
};
this.audio.onended = () => {
this.stop();
};
this.audio.ontimeupdate = () => {
this.duration = (this.audio.duration !== Infinity && !isNaN(this.audio.duration)) ? this.audio.duration : 0;
this.currentTime = this.audio.currentTime;
};
this.audio.volume = getLogarithmicVolume(this.volume);
this.audio.muted = this.isMuted;
if (this.current.isHls) {
// HLS playback support
if (Hls.isSupported()) {
this.hls = new Hls();
this.hls.loadSource(this.current.url);
this.hls.attachMedia(this.audio);
} else if (this.audio.canPlayType('application/vnd.apple.mpegurl')) {
this.audio.src = this.current.url;
} else {
console.log('Your browser does not support HLS.');
}
} else {
// Standard streams
this.audio.src = this.current.url;
// Firefox caches the downloaded stream, this causes playback issues.
// Giving the browser a new url on each start bypasses the old cache/buffer
if (navigator.userAgent.includes("Firefox")) {
this.audio.src += "?refresh=" + Date.now();
}
}
this.audio.load();
this.audio.play();
});
},
toggle(url, isStream, isHls) {
this.store.toggle({
url: url,
isStream: isStream,
isHls: isHls,
});
},
getCurrentTime() {
return this.currentTime;
},
getDuration() {
return this.duration;
},
getProgress() {
return (this.duration !== 0) ? +((this.currentTime / this.duration) * 100).toFixed(2) : 0;
},
setProgress(progress) {
if (this.audio !== null) {
this.audio.currentTime = (progress / 100) * this.duration;
}
},
isMuted: {
type: Boolean,
default: false
}
});
const $audio = ref(null);
const hls = ref(null);
const duration = ref(0);
const currentTime = ref(0);
const store = usePlayerStore();
const isPlaying = toRef(store, 'isPlaying');
const current = toRef(store, 'current');
watch(toRef(props, 'volume'), (newVol) => {
if ($audio.value !== null) {
$audio.value.volume = getLogarithmicVolume(newVol);
}
});
watch(toRef(props, 'isMuted'), (newMuted) => {
if ($audio.value !== null) {
$audio.value.muted = newMuted;
}
});
const stop = () => {
if ($audio.value !== null) {
$audio.value.pause();
$audio.value.src = '';
}
if (hls.value !== null) {
hls.value.destroy();
hls.value = null;
}
duration.value = 0;
currentTime.value = 0;
store.stopPlaying();
};
const play = () => {
if (isPlaying.value) {
stop();
nextTick(() => {
play();
});
return;
}
store.startPlaying();
nextTick(() => {
// Handle audio errors.
$audio.value.onerror = (e) => {
if (e.target.error.code === e.target.error.MEDIA_ERR_NETWORK && $audio.value.src !== '') {
console.log('Network interrupted stream. Automatically reconnecting shortly...');
setTimeout(() => {
play();
}, 5000);
}
};
$audio.value.onended = () => {
stop();
};
$audio.value.ontimeupdate = () => {
duration.value = ($audio.value.duration !== Infinity && !isNaN($audio.value.duration)) ? $audio.value.duration : 0;
currentTime.value = $audio.value.currentTime;
};
$audio.value.volume = getLogarithmicVolume(props.volume);
$audio.value.muted = props.isMuted;
if (current.value.isHls) {
// HLS playback support
if (Hls.isSupported()) {
hls.value = new Hls();
hls.value.loadSource(current.value.url);
hls.value.attachMedia($audio.value);
} else if (this.audio.canPlayType('application/vnd.apple.mpegurl')) {
$audio.value.src = current.value.url;
} else {
console.log('Your browser does not support HLS.');
}
} else {
// Standard streams
$audio.value.src = current.value.url;
// Firefox caches the downloaded stream, this causes playback issues.
// Giving the browser a new url on each start bypasses the old cache/buffer
if (navigator.userAgent.includes("Firefox")) {
$audio.value.src += "?refresh=" + Date.now();
}
}
$audio.value.load();
$audio.value.play();
});
};
const toggle = (url, isStream, isHls) => {
store.toggle({
url: url,
isStream: isStream,
isHls: isHls,
});
};
watch(current, (newCurrent) => {
if (newCurrent.url === null) {
stop();
} else {
play();
}
});
const getCurrentTime = () => currentTime.value;
const getDuration = () => duration.value;
const getProgress = () => {
return (duration.value !== 0)
? +((currentTime.value / duration.value) * 100).toFixed(2)
: 0;
};
const setProgress = (progress) => {
if ($audio.value !== null) {
$audio.value.currentTime = (progress / 100) * duration.value;
}
};
onMounted(() => {
// Allow pausing from the mobile metadata update.
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('pause', () => {
stop();
});
}
});
defineExpose({
play,
stop,
toggle,
getCurrentTime,
getDuration,
getProgress,
setProgress
});
</script>

View File

@ -35,113 +35,116 @@
</date-range-picker>
</template>
<script>
<script setup>
import DateRangePicker from 'vue3-daterange-picker';
import Icon from "./Icon";
import {DateTime} from 'luxon';
import {computed} from "vue";
import {useTranslate} from "~/vendor/gettext";
/* TODO Options API */
const props = defineProps({
tz: {
type: String,
default: 'system'
},
minDate: {
type: [String, Date],
default() {
return null
}
},
maxDate: {
type: [String, Date],
default() {
return null
}
},
timePicker: {
type: Boolean,
default: false,
},
modelValue: {
type: Object,
required: true
},
customRanges: {
type: [Object, Boolean],
default: null,
}
});
const emit = defineEmits(['update:modelValue', 'update']);
const dateRange = computed({
get: () => {
return props.modelValue;
},
set: () => {
// Noop
}
});
const {$gettext} = useTranslate();
const ranges = computed(() => {
if (null !== props.customRanges) {
return props.customRanges;
}
let nowTz = DateTime.now().setZone(props.tz);
let nowAtMidnightDate = nowTz.endOf('day').toJSDate();
let ranges = {};
ranges[$gettext('Last 24 Hours')] = [
nowTz.minus({days: 1}).toJSDate(),
nowTz.toJSDate()
];
ranges[$gettext('Today')] = [
nowTz.minus({days: 1}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[$gettext('Yesterday')] = [
nowTz.minus({days: 2}).startOf('day').toJSDate(),
nowTz.minus({days: 1}).endOf('day').toJSDate()
];
ranges[$gettext('Last 7 Days')] = [
nowTz.minus({days: 7}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[$gettext('Last 14 Days')] = [
nowTz.minus({days: 14}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[$gettext('Last 30 Days')] = [
nowTz.minus({days: 30}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[$gettext('This Month')] = [
nowTz.startOf('month').startOf('day').toJSDate(),
nowTz.endOf('month').endOf('day').toJSDate()
];
ranges[$gettext('Last Month')] = [
nowTz.minus({months: 1}).startOf('month').startOf('day').toJSDate(),
nowTz.minus({months: 1}).endOf('month').endOf('day').toJSDate()
];
return ranges;
});
const onSelect = (range) => {
emit('update:modelValue', range);
emit('update', range);
};
</script>
<script>
export default {
name: 'DateRangeDropdown',
components: {DateRangePicker, Icon},
inheritAttrs: false,
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
props: {
tz: {
type: String,
default: 'system'
},
minDate: {
type: [String, Date],
default() {
return null
}
},
maxDate: {
type: [String, Date],
default() {
return null
}
},
timePicker: {
type: Boolean,
default: false,
},
modelValue: {
type: Object,
required: true
},
customRanges: {
type: [Object, Boolean],
default: null,
},
},
emits: ['update:modelValue', 'update'],
computed: {
dateRange: {
get() {
return this.modelValue;
},
set() {
// Noop
}
},
ranges() {
let ranges = {};
if (null !== this.customRanges) {
return this.customRanges;
}
let nowTz = DateTime.now().setZone(this.tz);
let nowAtMidnightDate = nowTz.endOf('day').toJSDate();
ranges[this.$gettext('Last 24 Hours')] = [
nowTz.minus({days: 1}).toJSDate(),
nowTz.toJSDate()
];
ranges[this.$gettext('Today')] = [
nowTz.minus({days: 1}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[this.$gettext('Yesterday')] = [
nowTz.minus({days: 2}).startOf('day').toJSDate(),
nowTz.minus({days: 1}).endOf('day').toJSDate()
];
ranges[this.$gettext('Last 7 Days')] = [
nowTz.minus({days: 7}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[this.$gettext('Last 14 Days')] = [
nowTz.minus({days: 14}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[this.$gettext('Last 30 Days')] = [
nowTz.minus({days: 30}).startOf('day').toJSDate(),
nowAtMidnightDate
];
ranges[this.$gettext('This Month')] = [
nowTz.startOf('month').startOf('day').toJSDate(),
nowTz.endOf('month').endOf('day').toJSDate()
];
ranges[this.$gettext('Last Month')] = [
nowTz.minus({months: 1}).startOf('month').startOf('day').toJSDate(),
nowTz.minus({months: 1}).endOf('month').endOf('day').toJSDate()
];
return ranges;
}
},
methods: {
onSelect(range) {
this.$emit('update:modelValue', range);
this.$emit('update', range);
}
}
}
</script>

View File

@ -13,41 +13,27 @@
</div>
</template>
<script>
/* TODO Options API */
<script setup>
import {useAsyncState} from "@vueuse/core";
import {useAxios} from "~/vendor/axios";
export default {
name: 'LogList',
props: {
url: {
type: String,
required: true
},
const props = defineProps({
url: {
type: String,
required: true
},
emits: ['view'],
data() {
return {
loading: true,
logs: []
}
},
mounted() {
this.relist();
},
methods: {
relist() {
this.loading = true;
this.$wrapWithLoading(
this.axios.get(this.url)
).then((resp) => {
this.logs = resp.data.logs;
}).finally(() => {
this.loading = false;
});
},
viewLog(url) {
this.$emit('view', url);
}
}
}
});
const emit = defineEmits(['view']);
const {axios} = useAxios();
const {state: logs} = useAsyncState(
() => axios.get(props.url).then((r) => r.data.logs),
[]
);
const viewLog = (url) => {
emit('view', url);
};
</script>

View File

@ -1,7 +1,7 @@
<template>
<b-modal
:id="id"
ref="modal"
ref="$modal"
:size="size"
:centered="centered"
:title="title"
@ -44,7 +44,7 @@
<b-button
variant="default"
type="button"
@click="close"
@click="hide"
>
{{ $gettext('Close') }}
</b-button>
@ -61,7 +61,7 @@
</template>
<template
v-for="(_, slot) of filteredScopedSlots"
v-for="(_, slot) of useSlotsExcept($slots, ['default', 'modal-footer'])"
#[slot]="scope"
>
<slot
@ -72,78 +72,71 @@
</b-modal>
</template>
<script>
<script setup>
import InvisibleSubmitButton from "~/components/Common/InvisibleSubmitButton.vue";
import {defineComponent} from "vue";
import {filter, includes} from "lodash";
import {ref} from "vue";
import useSlotsExcept from "~/functions/useSlotsExcept";
/* TODO Options API */
export default defineComponent({
components: {InvisibleSubmitButton},
props: {
title: {
type: String,
required: true
},
size: {
type: String,
default: 'lg'
},
centered: {
type: Boolean,
default: false
},
id: {
type: String,
default: 'edit-modal'
},
loading: {
type: Boolean,
default: false
},
disableSaveButton: {
type: Boolean,
default: false
},
noEnforceFocus: {
type: Boolean,
default: false,
},
error: {
type: String,
default: null
}
const props = defineProps({
title: {
type: String,
required: true
},
emits: ['submit', 'shown', 'hidden'],
computed: {
filteredScopedSlots() {
return filter(this.$slots, (slot, name) => {
return !includes([
'default', 'modal-footer'
], name);
});
},
size: {
type: String,
default: 'lg'
},
methods: {
doSubmit() {
this.$emit('submit');
},
onShown() {
this.$emit('shown');
},
onHidden() {
this.$emit('hidden');
},
close() {
this.hide();
},
hide() {
this.$refs.modal.hide();
},
show() {
this.$refs.modal.show();
}
centered: {
type: Boolean,
default: false
},
id: {
type: String,
default: 'edit-modal'
},
loading: {
type: Boolean,
default: false
},
disableSaveButton: {
type: Boolean,
default: false
},
noEnforceFocus: {
type: Boolean,
default: false,
},
error: {
type: String,
default: null
}
});
const emit = defineEmits(['submit', 'shown', 'hidden']);
const doSubmit = () => {
emit('submit');
};
const onShown = () => {
emit('shown');
};
const onHidden = () => {
emit('hidden');
};
const $modal = ref(); // Template Ref
const hide = () => {
$modal.value.hide();
};
const show = () => {
$modal.value.show();
};
defineExpose({
show
});
</script>

View File

@ -39,95 +39,104 @@
</div>
</template>
<script>
<script setup>
import DataTable from '~/components/Common/DataTable';
import {forEach} from 'lodash';
import AlbumArt from '~/components/Common/AlbumArt';
import {computed} from "vue";
import {useTranslate} from "~/vendor/gettext";
import {useAxios} from "~/vendor/axios";
import {useNotify} from "~/vendor/bootstrapVue";
/* TODO Options API */
export default {
components: {AlbumArt, DataTable},
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
},
emits: ['submitted'],
data () {
let fields = [
{key: 'name', isRowHeader: true, label: this.$gettext('Name'), sortable: true, selectable: true},
{
key: 'song.title',
label: this.$gettext('Title'),
sortable: true,
selectable: true,
visible: false,
},
{
key: 'song.artist',
label: this.$gettext('Artist'),
sortable: true,
selectable: true,
visible: false,
},
{
key: 'song.album',
label: this.$gettext('Album'),
sortable: true,
selectable: true,
visible: false
},
{
key: 'song.genre',
label: this.$gettext('Genre'),
sortable: true,
selectable: true,
visible: false
}
];
forEach(this.customFields.slice(), (field) => {
fields.push({
key: 'song.custom_fields.' + field.short_name,
label: field.name,
sortable: false,
selectable: true,
visible: false
});
});
fields.push(
{key: 'actions', label: this.$gettext('Actions'), class: 'shrink', sortable: false}
);
return {
fields: fields,
pageOptions: [
10, 25
]
};
showAlbumArt: {
type: Boolean,
default: true
},
methods: {
doSubmitRequest (url) {
this.axios.post(url).then((resp) => {
this.$notifySuccess(resp.data.message);
this.$emit('submitted');
}).catch(() => {
this.$emit('submitted');
});
}
customFields: {
type: Array,
required: false,
default: () => []
}
});
const emit = defineEmits(['submitted']);
const {$gettext} = useTranslate();
const fields = computed(() => {
let fields = [
{
key: 'name',
isRowHeader: true,
label: $gettext('Name'),
sortable: true,
selectable: true
},
{
key: 'song.title',
label: $gettext('Title'),
sortable: true,
selectable: true,
visible: false,
},
{
key: 'song.artist',
label: $gettext('Artist'),
sortable: true,
selectable: true,
visible: false,
},
{
key: 'song.album',
label: $gettext('Album'),
sortable: true,
selectable: true,
visible: false
},
{
key: 'song.genre',
label: $gettext('Genre'),
sortable: true,
selectable: true,
visible: false
}
];
forEach({...props.customFields}, (field) => {
fields.push({
key: 'song.custom_fields.' + field.short_name,
label: field.name,
sortable: false,
selectable: true,
visible: false
});
});
fields.push(
{key: 'actions', label: $gettext('Actions'), class: 'shrink', sortable: false}
);
return fields;
});
const pageOptions = [10, 25];
const {wrapWithLoading, notifySuccess} = useNotify();
const {axios} = useAxios();
const doSubmitRequest = (url) => {
wrapWithLoading(
axios.post(url)
).then((resp) => {
notifySuccess(resp.data.message);
}).finally(() => {
emit('submitted');
});
};
</script>