Add store types and fix other type errors
This commit is contained in:
parent
9e0596d136
commit
0b53ec5b1c
|
@ -42,7 +42,7 @@
|
||||||
"vue-plyr": "7.0.0",
|
"vue-plyr": "7.0.0",
|
||||||
"vue-router": "4.0.14",
|
"vue-router": "4.0.14",
|
||||||
"vue-tsc": "0.34.7",
|
"vue-tsc": "0.34.7",
|
||||||
"vue-upload-component": "2.8.22",
|
"vue-upload-component": "3.1.2",
|
||||||
"vue3-gettext": "2.2.0-alpha.1",
|
"vue3-gettext": "2.2.0-alpha.1",
|
||||||
"vue3-lazyload": "0.2.5-beta",
|
"vue3-lazyload": "0.2.5-beta",
|
||||||
"vuedraggable": "4.1.0",
|
"vuedraggable": "4.1.0",
|
||||||
|
@ -55,27 +55,26 @@
|
||||||
"@types/jquery": "3.5.14",
|
"@types/jquery": "3.5.14",
|
||||||
"@types/lodash-es": "4.17.6",
|
"@types/lodash-es": "4.17.6",
|
||||||
"@types/qs": "6.9.7",
|
"@types/qs": "6.9.7",
|
||||||
"@typescript-eslint/eslint-plugin": "5.19.0",
|
"@typescript-eslint/eslint-plugin": "5.21.0",
|
||||||
"@vitejs/plugin-vue": "2.3.1",
|
"@vitejs/plugin-vue": "2.3.1",
|
||||||
"@vue/compiler-sfc": "3.2.33",
|
"@vue/compiler-sfc": "3.2.33",
|
||||||
"@vue/eslint-config-standard": "6.1.0",
|
"@vue/eslint-config-standard": "6.1.0",
|
||||||
"@vue/eslint-config-typescript": "10.0.0",
|
"@vue/eslint-config-typescript": "10.0.0",
|
||||||
"@vue/test-utils": "1.3.0",
|
"@vue/test-utils": "1.3.0",
|
||||||
"autoprefixer": "10.4.4",
|
|
||||||
"chai": "4.3.6",
|
"chai": "4.3.6",
|
||||||
"easygettext": "2.17.0",
|
"easygettext": "2.17.0",
|
||||||
"eslint": "8.11.0",
|
"eslint": "8.14.0",
|
||||||
"eslint-config-standard": "16.0.3",
|
"eslint-config-standard": "17.0.0",
|
||||||
"eslint-plugin-html": "6.2.0",
|
"eslint-plugin-html": "6.2.0",
|
||||||
"eslint-plugin-import": "2.25.4",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-node": "11.1.0",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"eslint-plugin-promise": "6.0.0",
|
"eslint-plugin-promise": "6.0.0",
|
||||||
"eslint-plugin-vue": "7.20.0",
|
"eslint-plugin-vue": "8.7.1",
|
||||||
"jest-cli": "27.5.1",
|
"jest-cli": "27.5.1",
|
||||||
"moxios": "0.4.0",
|
"moxios": "0.4.0",
|
||||||
"sinon": "13.0.2",
|
"sinon": "13.0.2",
|
||||||
"ts-jest": "27.1.4",
|
"ts-jest": "27.1.4",
|
||||||
"typescript": "^4.6.3",
|
"typescript": "4.6.3",
|
||||||
"vite": "2.8.6",
|
"vite": "2.8.6",
|
||||||
"vite-plugin-pwa": "0.12.0",
|
"vite-plugin-pwa": "0.12.0",
|
||||||
"vue-jest": "3.0.7",
|
"vue-jest": "3.0.7",
|
||||||
|
|
|
@ -12,7 +12,6 @@ import ReportModal from '~/components/moderation/ReportModal.vue'
|
||||||
import { useIntervalFn, useToggle, useWindowSize } from '@vueuse/core'
|
import { useIntervalFn, useToggle, useWindowSize } from '@vueuse/core'
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
||||||
import store from '~/store'
|
|
||||||
import {
|
import {
|
||||||
ListenWSEvent,
|
ListenWSEvent,
|
||||||
PendingReviewEditsWSEvent,
|
PendingReviewEditsWSEvent,
|
||||||
|
@ -21,8 +20,11 @@ import {
|
||||||
Track
|
Track
|
||||||
} from '~/types'
|
} from '~/types'
|
||||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||||
import { getClientOnlyRadio } from '~/radios'
|
import { CLIENT_RADIOS } from '~/utils/clientRadios'
|
||||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
// Tracks
|
// Tracks
|
||||||
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
||||||
|
@ -49,7 +51,7 @@ watchEffect(() => {
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
const customStylesheets = computed(() => {
|
const customStylesheets = computed(() => {
|
||||||
return store.state.instance?.frontSettings?.additionalStylesheets ?? []
|
return store.state.instance.frontSettings.additionalStylesheets ?? []
|
||||||
})
|
})
|
||||||
|
|
||||||
// Fake content
|
// Fake content
|
||||||
|
@ -93,10 +95,10 @@ useWebSocketHandler('user_request.created', (event) => {
|
||||||
|
|
||||||
useWebSocketHandler('Listen', (event) => {
|
useWebSocketHandler('Listen', (event) => {
|
||||||
if (store.state.radios.current && store.state.radios.running) {
|
if (store.state.radios.current && store.state.radios.running) {
|
||||||
const { current } = store.state.radios
|
const current = store.state.radios.current
|
||||||
|
|
||||||
if (current.clientOnly && current.type === 'account') {
|
if (current?.clientOnly) {
|
||||||
getClientOnlyRadio(current).handleListen(current, event as ListenWSEvent, store)
|
CLIENT_RADIOS[current.type].handleListen(current, event as ListenWSEvent, store)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -345,7 +345,8 @@
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters, mapActions, useStore } from 'vuex'
|
import { useStore } from '~/store'
|
||||||
|
import { mapState, mapGetters, mapActions } from 'vuex'
|
||||||
import { nextTick, onMounted, ref, computed } from 'vue'
|
import { nextTick, onMounted, ref, computed } from 'vue'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { sum } from 'lodash-es'
|
import { sum } from 'lodash-es'
|
||||||
|
|
|
@ -140,7 +140,7 @@ export default {
|
||||||
instances.push(serverUrl)
|
instances.push(serverUrl)
|
||||||
}
|
}
|
||||||
const self = this
|
const self = this
|
||||||
instances.push(this.$store.getters['instance/defaultUrl'](), 'https://demo.funkwhale.audio/')
|
instances.push(location.origin, 'https://demo.funkwhale.audio/')
|
||||||
return uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl }))
|
return uniq(instances.filter((e) => { return e !== self.$store.state.instance.instanceUrl }))
|
||||||
},
|
},
|
||||||
instanceHostname () {
|
instanceHostname () {
|
||||||
|
|
|
@ -51,8 +51,8 @@
|
||||||
</template>
|
</template>
|
||||||
<content-form
|
<content-form
|
||||||
v-if="setting.fieldType === 'markdown'"
|
v-if="setting.fieldType === 'markdown'"
|
||||||
v-bind="setting.fieldParams"
|
|
||||||
v-model="values[setting.identifier]"
|
v-model="values[setting.identifier]"
|
||||||
|
v-bind="setting.fieldParams"
|
||||||
/>
|
/>
|
||||||
<signup-form-builder
|
<signup-form-builder
|
||||||
v-else-if="setting.fieldType === 'formBuilder'"
|
v-else-if="setting.fieldType === 'formBuilder'"
|
||||||
|
@ -158,6 +158,7 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { cloneDeep } from 'lodash-es'
|
import { cloneDeep } from 'lodash-es'
|
||||||
import SignupFormBuilder from '~/components/admin/SignupFormBuilder.vue'
|
import SignupFormBuilder from '~/components/admin/SignupFormBuilder.vue'
|
||||||
|
import useFormData from '~/composables/useFormData'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -207,20 +208,27 @@ export default {
|
||||||
let contentType = 'application/json'
|
let contentType = 'application/json'
|
||||||
const fileSettingsIDs = this.fileSettings.map((s) => { return s.identifier })
|
const fileSettingsIDs = this.fileSettings.map((s) => { return s.identifier })
|
||||||
if (fileSettingsIDs.length > 0) {
|
if (fileSettingsIDs.length > 0) {
|
||||||
contentType = 'multipart/form-data'
|
const data = this.settings.reduce((data, setting) => {
|
||||||
postData = new FormData()
|
if (fileSettingsIDs.includes(setting.identifier)) {
|
||||||
this.settings.forEach((s) => {
|
const [input] = this.$refs[setting.identifier]
|
||||||
if (fileSettingsIDs.indexOf(s.identifier) > -1) {
|
const { files } = input
|
||||||
const input = self.$refs[s.identifier][0]
|
|
||||||
const files = input.files
|
// TODO (wvffle): Move to the top of setup
|
||||||
console.log('ref', input, files)
|
const logger = useLogger()
|
||||||
if (files && files.length > 0) {
|
logger.debug('ref', input, files)
|
||||||
postData.append(s.identifier, files[0])
|
|
||||||
|
if (files?.length > 0) {
|
||||||
|
data[s.identifier] = files[0]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
postData.append(s.identifier, self.values[s.identifier])
|
postData.append(s.identifier, this.values[s.identifier])
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
return data
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
contentType = 'multipart/form-data'
|
||||||
|
postData = useFormData(data)
|
||||||
}
|
}
|
||||||
axios.post('instance/admin/settings/bulk/', postData, {
|
axios.post('instance/admin/settings/bulk/', postData, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
|
@ -320,7 +320,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useStore, mapState, mapGetters, mapActions } from 'vuex'
|
import { useStore } from '~/store'
|
||||||
|
import { mapState, mapGetters, mapActions } from 'vuex'
|
||||||
import toLinearVolumeScale from '~/composables/audio/toLinearVolumeScale'
|
import toLinearVolumeScale from '~/composables/audio/toLinearVolumeScale'
|
||||||
import { Howl, Howler } from 'howler'
|
import { Howl, Howler } from 'howler'
|
||||||
import { throttle, reverse } from 'lodash-es'
|
import { throttle, reverse } from 'lodash-es'
|
||||||
|
|
|
@ -146,6 +146,8 @@ import axios from 'axios'
|
||||||
|
|
||||||
import { checkRedirectToLogin } from '~/utils'
|
import { checkRedirectToLogin } from '~/utils'
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
import useFormData from '~/composables/useFormData'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
clientId: { type: String, required: true },
|
clientId: { type: String, required: true },
|
||||||
|
@ -252,14 +254,16 @@ export default {
|
||||||
submit () {
|
submit () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
const self = this
|
||||||
const data = new FormData()
|
const data = useFormData({
|
||||||
data.set('redirect_uri', this.redirectUri)
|
redirect_uri: this.redirectUri,
|
||||||
data.set('scope', this.scope)
|
scope: this.scope,
|
||||||
data.set('allow', true)
|
allow: true,
|
||||||
data.set('client_id', this.clientId)
|
client_id: this.clientId,
|
||||||
data.set('response_type', this.responseType)
|
response_type: this.responseType,
|
||||||
data.set('state', this.state)
|
state: this.state,
|
||||||
data.set('nonce', this.nonce)
|
nonce: this.nonce
|
||||||
|
})
|
||||||
|
|
||||||
axios.post('oauth/authorize/', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }).then((response) => {
|
axios.post('oauth/authorize/', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest' } }).then((response) => {
|
||||||
if (self.redirectUri === 'urn:ietf:wg:oauth:2.0:oob') {
|
if (self.redirectUri === 'urn:ietf:wg:oauth:2.0:oob') {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="$store.getters['instance/appDomain'] === $store.getters['instance/domain']">
|
<template v-if="document.domain === $store.getters['instance/domain']">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="username-field">
|
<label for="username-field">
|
||||||
<translate translate-context="Content/Login/Input.Label/Noun">Username or e-mail address</translate>
|
<translate translate-context="Content/Login/Input.Label/Noun">Username or e-mail address</translate>
|
||||||
|
@ -133,7 +133,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.$store.getters['instance/appDomain'] === this.$store.getters['instance/domain']) {
|
if (document.domain === this.$store.getters['instance/domain']) {
|
||||||
return await this.submitSession()
|
return await this.submitSession()
|
||||||
} else {
|
} else {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
@ -141,8 +141,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submitSession () {
|
async submitSession () {
|
||||||
const self = this
|
this.isLoading = true
|
||||||
self.isLoading = true
|
|
||||||
this.error = ''
|
this.error = ''
|
||||||
const credentials = {
|
const credentials = {
|
||||||
username: this.credentials.username,
|
username: this.credentials.username,
|
||||||
|
@ -160,7 +159,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(e => {
|
.then(() => {
|
||||||
self.isLoading = false
|
self.isLoading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,16 +205,15 @@
|
||||||
</div>
|
</div>
|
||||||
<file-upload-widget
|
<file-upload-widget
|
||||||
ref="upload"
|
ref="upload"
|
||||||
|
v-model="filesModel"
|
||||||
:class="['ui', 'icon', 'basic', 'button', 'channels', {hidden: step === 3}]"
|
:class="['ui', 'icon', 'basic', 'button', 'channels', {hidden: step === 3}]"
|
||||||
:post-action="uploadUrl"
|
:post-action="uploadUrl"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:data="baseImportMetadata"
|
:data="baseImportMetadata"
|
||||||
:drop="true"
|
:drop="true"
|
||||||
:extensions="$store.state.ui.supportedExtensions"
|
:extensions="$store.state.ui.supportedExtensions"
|
||||||
:value="files"
|
|
||||||
name="audio_file"
|
name="audio_file"
|
||||||
:thread="1"
|
:thread="1"
|
||||||
@input="updateFiles"
|
|
||||||
@input-file="inputFile"
|
@input-file="inputFile"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
@ -292,6 +291,15 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
filesModel: {
|
||||||
|
get () {
|
||||||
|
return this.files
|
||||||
|
},
|
||||||
|
|
||||||
|
set (value) {
|
||||||
|
this.updateFiles(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
labels () {
|
labels () {
|
||||||
return {
|
return {
|
||||||
editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit')
|
editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit')
|
||||||
|
@ -419,9 +427,7 @@ export default {
|
||||||
return uploaded
|
return uploaded
|
||||||
},
|
},
|
||||||
activeFile () {
|
activeFile () {
|
||||||
return this.files.filter((f) => {
|
return this.files.find((file) => file.active)
|
||||||
return f.active
|
|
||||||
})[0]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -585,15 +591,14 @@ export default {
|
||||||
this[objName] = Object.assign({}, this[objName], newData)
|
this[objName] = Object.assign({}, this[objName], newData)
|
||||||
},
|
},
|
||||||
updateFiles (value) {
|
updateFiles (value) {
|
||||||
const self = this
|
|
||||||
this.files = value
|
this.files = value
|
||||||
this.files.forEach((f) => {
|
this.files.forEach((f) => {
|
||||||
if (f.response && f.response.uuid && self.audioMetadata[f.response.uuid] === undefined) {
|
if (f.response?.uuid && this.audioMetadata[f.response.uuid] === undefined) {
|
||||||
self.uploadData[f.response.uuid] = f.response
|
this.uploadData[f.response.uuid] = f.response
|
||||||
self.setDynamic('uploadImportData', f.response.uuid, {
|
this.setDynamic('uploadImportData', f.response.uuid, {
|
||||||
...f.response.import_metadata
|
...f.response.import_metadata
|
||||||
})
|
})
|
||||||
self.fetchAudioMetadata(f.response.uuid)
|
this.fetchAudioMetadata(f.response.uuid)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,8 @@ import axios from 'axios'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { reactive, ref, watch } from 'vue'
|
import { reactive, ref, watch } from 'vue'
|
||||||
import { BackendError } from '~/types'
|
import { BackendError } from '~/types'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from '~/store'
|
||||||
|
import useFormData from '~/composables/useFormData'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string | null
|
modelValue: string | null
|
||||||
|
@ -35,8 +36,7 @@ const submit = async () => {
|
||||||
errors.length = 0
|
errors.length = 0
|
||||||
file.value = input.value.files[0]
|
file.value = input.value.files[0]
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = useFormData({ file: file.value })
|
||||||
formData.append('file', file.value)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.post('attachments/', formData, {
|
const { data } = await axios.post('attachments/', formData, {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useToggle } from '@vueuse/core'
|
import { useToggle } from '@vueuse/core'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import $ from 'jquery'
|
import $ from 'jquery'
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import store from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
content: string
|
content: string
|
||||||
|
@ -10,6 +10,7 @@ interface Message {
|
||||||
|
|
||||||
const props = defineProps<{ message: Message }>()
|
const props = defineProps<{ message: Message }>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const params = {
|
const params = {
|
||||||
context: '#app',
|
context: '#app',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useGettext } from 'vue3-gettext'
|
import { useGettext } from 'vue3-gettext'
|
||||||
import { useClipboard, useVModel } from '@vueuse/core'
|
import { useClipboard, useVModel } from '@vueuse/core'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: string
|
modelValue: string
|
||||||
|
|
|
@ -1,44 +1,72 @@
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import FileUpload from 'vue-upload-component'
|
import FileUpload, { VueUploadItem } from 'vue-upload-component'
|
||||||
import { setCsrf } from '~/utils'
|
import { getCookie } from '~/utils'
|
||||||
|
import { computed, getCurrentInstance } from 'vue'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
export default {
|
const instance = getCurrentInstance()
|
||||||
extends: FileUpload,
|
const attrs = instance?.attrs ?? {}
|
||||||
methods: {
|
|
||||||
uploadHtml5 (file) {
|
const store = useStore()
|
||||||
const form = new window.FormData()
|
const headers = computed(() => {
|
||||||
const filename = file.file.filename || file.name
|
const headers: Record<string, string> = typeof attrs.headers === 'object'
|
||||||
let value
|
? { ...attrs.headers }
|
||||||
const data = { ...file.data }
|
: {}
|
||||||
if (data.import_metadata) {
|
|
||||||
data.import_metadata = { ...(data.import_metadata || {}) }
|
if (store.state.auth.oauth.accessToken) {
|
||||||
if (data.channel && !data.import_metadata.title) {
|
headers.Authorization ??= store.getters['auth/header']
|
||||||
data.import_metadata.title = filename.replace(/\.[^/.]+$/, '')
|
|
||||||
}
|
|
||||||
data.import_metadata = JSON.stringify(data.import_metadata)
|
|
||||||
}
|
|
||||||
for (const key in data) {
|
|
||||||
value = data[key]
|
|
||||||
if (value && typeof value === 'object' && typeof value.toString !== 'function') {
|
|
||||||
if (value instanceof File) {
|
|
||||||
form.append(key, value, value.name)
|
|
||||||
} else {
|
|
||||||
form.append(key, JSON.stringify(value))
|
|
||||||
}
|
|
||||||
} else if (value !== null && value !== undefined) {
|
|
||||||
form.append(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
form.append('source', `upload://${filename}`)
|
|
||||||
form.append(this.name, file.file, filename)
|
|
||||||
const xhr = new XMLHttpRequest()
|
|
||||||
xhr.open('POST', file.postAction)
|
|
||||||
setCsrf(xhr)
|
|
||||||
if (this.$store.state.auth.oauth.accessToken) {
|
|
||||||
xhr.setRequestHeader('Authorization', this.$store.getters['auth/header'])
|
|
||||||
}
|
|
||||||
return this.uploadXhr(xhr, file, form)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const csrf = getCookie('csrftoken')
|
||||||
|
if (csrf) headers['X-CSRFToken'] = csrf
|
||||||
|
|
||||||
|
return headers
|
||||||
|
})
|
||||||
|
|
||||||
|
const patchFileData = (file: VueUploadItem, data: Record<string, unknown> = {}) => {
|
||||||
|
let metadata = data.import_metadata as Record<string, unknown>
|
||||||
|
|
||||||
|
// @ts-expect-error Taken from vue-upload-component@3.1.2
|
||||||
|
const filename: string = file.file.name || file.file.filename || file.name
|
||||||
|
data.source = `upload://${filename}`
|
||||||
|
|
||||||
|
if (metadata) {
|
||||||
|
metadata = { ...metadata }
|
||||||
|
if (data.channel && !metadata.title) {
|
||||||
|
metadata.title = filename.replace(/\.[^/.]+$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
data.import_metadata = JSON.stringify(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadAction = async (file: VueUploadItem, self: any): Promise<VueUploadItem> => {
|
||||||
|
file.data = patchFileData(file, file.data)
|
||||||
|
|
||||||
|
// NOTE: We're only patching the file data. The rest of the process should remain the same:
|
||||||
|
// https://github.com/lian-yue/vue-upload-component/blob/1bd3be3a56e8ed2934dbe0beae151e9026ca51f9/src/FileUpload.vue#L973-L987
|
||||||
|
if (self.features.html5) {
|
||||||
|
if (self.shouldUseChunkUpload(file)) return self.uploadChunk(file)
|
||||||
|
if (file.putAction) return self.uploadPut(file)
|
||||||
|
if (file.postAction) return self.uploadHtml5(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.postAction) return self.uploadHtml4(file)
|
||||||
|
return Promise.reject(new Error('No action configured'))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
// NOTE: We're disallowing overriding `custom-action` and `headers` props
|
||||||
|
export default { inheritAttrs: false }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<file-upload
|
||||||
|
v-bind="$attrs"
|
||||||
|
:custom-action="uploadAction"
|
||||||
|
:headers="headers"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
|
@ -90,11 +90,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { mapState, useStore } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import Modal from '~/components/semantic/Modal.vue'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
|
|
|
@ -155,10 +155,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { mapState, useStore } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
|
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
|
||||||
import Modal from '~/components/semantic/Modal.vue'
|
import Modal from '~/components/semantic/Modal.vue'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
function urlDomain (data) {
|
function urlDomain (data) {
|
||||||
const a = document.createElement('a')
|
const a = document.createElement('a')
|
||||||
|
|
|
@ -3,7 +3,7 @@ import $ from 'jquery'
|
||||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||||
import { computed, onBeforeUnmount, ref, watchEffect } from 'vue'
|
import { computed, onBeforeUnmount, ref, watchEffect } from 'vue'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
show: boolean
|
show: boolean
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
export default (object?: Record<string, string | File | object | null>) => {
|
||||||
|
const data = new FormData()
|
||||||
|
|
||||||
|
if (object) {
|
||||||
|
for (const [key, value] of Object.entries(object)) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
data.set(key, value)
|
||||||
|
} else if (value instanceof File) {
|
||||||
|
data.set(key, value, value.name)
|
||||||
|
} else {
|
||||||
|
data.set(key, JSON.stringify(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import store from '~/store'
|
import store from '~/store'
|
||||||
import { tryOnScopeDispose } from '@vueuse/core'
|
import { tryOnScopeDispose } from '@vueuse/core'
|
||||||
import { WebSocketEvent } from '~/types'
|
import { WebSocketEvent } from '~/types'
|
||||||
|
import { WebSocketEventName } from '~/store/ui'
|
||||||
|
|
||||||
export default (eventName: string, handler: (event: WebSocketEvent) => void) => {
|
export default (eventName: WebSocketEventName, handler: (event: WebSocketEvent) => void) => {
|
||||||
const id = `${+new Date() + Math.random()}`
|
const id = `${+new Date() + Math.random()}`
|
||||||
store.commit('ui/addWebsocketEventHandler', { eventName, handler, id })
|
store.commit('ui/addWebsocketEventHandler', { eventName, handler, id })
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const install: InitModule = async ({ store, router }) => {
|
||||||
// 3. use the current url
|
// 3. use the current url
|
||||||
const defaultInstanceUrl = store.state.instance.frontSettings.defaultServerUrl ||
|
const defaultInstanceUrl = store.state.instance.frontSettings.defaultServerUrl ||
|
||||||
import.meta.env.VUE_APP_INSTANCE_URL ||
|
import.meta.env.VUE_APP_INSTANCE_URL ||
|
||||||
store.getters['instance/defaultUrl']()
|
location.origin
|
||||||
|
|
||||||
store.commit('instance/instanceUrl', defaultInstanceUrl)
|
store.commit('instance/instanceUrl', defaultInstanceUrl)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { InitModule } from '~/types'
|
||||||
import store from '~/store'
|
import store from '~/store'
|
||||||
|
|
||||||
const defaultLanguage = store.state.ui.currentLanguage ?? 'en_US'
|
const defaultLanguage = store.state.ui.currentLanguage ?? 'en_US'
|
||||||
const availableLanguages = locales.reduce((map: Record<string, string>, locale) => {
|
export const availableLanguages = locales.reduce((map: Record<string, string>, locale) => {
|
||||||
map[locale.code] = locale.label
|
map[locale.code] = locale.label
|
||||||
return map
|
return map
|
||||||
}, {})
|
}, {})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import router from '~/router'
|
import router from '~/router'
|
||||||
import store from '~/store'
|
import store, { key } from '~/store'
|
||||||
// @ts-expect-error typescript does not know about configureCompat
|
// @ts-expect-error typescript does not know about configureCompat
|
||||||
import { configureCompat, createApp, defineAsyncComponent, h } from 'vue'
|
import { configureCompat, createApp, defineAsyncComponent, h } from 'vue'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
@ -33,7 +33,7 @@ const app = createApp({
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(store)
|
app.use(store, key)
|
||||||
|
|
||||||
const modules: Array<Promise<unknown>> = []
|
const modules: Array<Promise<unknown>> = []
|
||||||
for (const module of Object.values(import.meta.globEager('./init/*.ts'))) {
|
for (const module of Object.values(import.meta.globEager('./init/*.ts'))) {
|
||||||
|
@ -53,5 +53,6 @@ Promise.all(modules).finally(() => {
|
||||||
// TODO (wvffle): Check for mixin merging: https://v3-migration.vuejs.org/breaking-changes/data-option.html#mixin-merge-behavior-change=
|
// TODO (wvffle): Check for mixin merging: https://v3-migration.vuejs.org/breaking-changes/data-option.html#mixin-merge-behavior-change=
|
||||||
// TODO (wvffle): Use emits options: https://v3-migration.vuejs.org/breaking-changes/emits-option.html
|
// TODO (wvffle): Use emits options: https://v3-migration.vuejs.org/breaking-changes/emits-option.html
|
||||||
// TODO (wvffle): Find all array watchers and make them deep
|
// TODO (wvffle): Find all array watchers and make them deep
|
||||||
// TODO (wvffle): Migrate to <script setup>
|
// TODO (wvffle): Migrate to <script setup lang="ts"> and remove allowJs from tsconfig.json
|
||||||
// TODO (wvffle): Replace `from '(../)+` with `from '~/`
|
// TODO (wvffle): Replace `from '(../)+` with `from '~/`
|
||||||
|
// TODO (wvffle): Use navigation guards
|
||||||
|
|
|
@ -618,7 +618,7 @@ export default createRouter({
|
||||||
component: () =>
|
component: () =>
|
||||||
import('~/components/library/Home.vue'),
|
import('~/components/library/Home.vue'),
|
||||||
name: 'library.me',
|
name: 'library.me',
|
||||||
props: route => ({
|
props: () => ({
|
||||||
scope: 'me'
|
scope: 'me'
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,27 +1,51 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
import useFormData from '~/composables/useFormData'
|
||||||
|
|
||||||
|
export type Permission = 'settings' | 'library' | 'moderation'
|
||||||
|
export interface State {
|
||||||
|
authenticated: boolean
|
||||||
|
username: string
|
||||||
|
fullUsername: string
|
||||||
|
availablePermissions: Record<Permission, boolean>,
|
||||||
|
profile: null | Profile
|
||||||
|
oauth: OAuthTokens
|
||||||
|
scopedTokens: ScopedTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
id: string
|
||||||
|
avatar?: string
|
||||||
|
username: string
|
||||||
|
full_username: string
|
||||||
|
instance_support_message_display_date: string
|
||||||
|
funkwhale_support_message_display_date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScopedTokens {
|
||||||
|
listen: null | string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OAuthTokens {
|
||||||
|
clientId: null | string
|
||||||
|
clientSecret: null | string
|
||||||
|
accessToken: null | string
|
||||||
|
refreshToken: null | string
|
||||||
|
}
|
||||||
|
|
||||||
|
const NEEDED_SCOPES = 'read write'
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
function getDefaultScopedTokens () {
|
function getDefaultScopedTokens (): ScopedTokens {
|
||||||
return {
|
return {
|
||||||
listen: null
|
listen: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function asForm (obj) {
|
function getDefaultOauth (): OAuthTokens {
|
||||||
const data = new FormData()
|
|
||||||
Object.entries(obj).forEach(([key, value]) => {
|
|
||||||
data.set(key, value)
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
let baseUrl = `${window.location.protocol}//${window.location.hostname}`
|
|
||||||
if (window.location.port) {
|
|
||||||
baseUrl = `${baseUrl}:${window.location.port}`
|
|
||||||
}
|
|
||||||
function getDefaultOauth () {
|
|
||||||
return {
|
return {
|
||||||
clientId: null,
|
clientId: null,
|
||||||
clientSecret: null,
|
clientSecret: null,
|
||||||
|
@ -29,20 +53,18 @@ function getDefaultOauth () {
|
||||||
refreshToken: null
|
refreshToken: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const NEEDED_SCOPES = [
|
|
||||||
'read',
|
async function createOauthApp () {
|
||||||
'write'
|
|
||||||
].join(' ')
|
|
||||||
async function createOauthApp (domain) {
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: `Funkwhale web client at ${window.location.hostname}`,
|
name: `Funkwhale web client at ${window.location.hostname}`,
|
||||||
website: baseUrl,
|
website: location.origin,
|
||||||
scopes: NEEDED_SCOPES,
|
scopes: NEEDED_SCOPES,
|
||||||
redirect_uris: `${baseUrl}/auth/callback`
|
redirect_uris: `${location.origin}/auth/callback`
|
||||||
}
|
}
|
||||||
return (await axios.post('oauth/apps/', payload)).data
|
return (await axios.post('oauth/apps/', payload)).data
|
||||||
}
|
}
|
||||||
export default {
|
|
||||||
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
|
@ -73,10 +95,9 @@ export default {
|
||||||
state.scopedTokens = getDefaultScopedTokens()
|
state.scopedTokens = getDefaultScopedTokens()
|
||||||
state.oauth = getDefaultOauth()
|
state.oauth = getDefaultOauth()
|
||||||
state.availablePermissions = {
|
state.availablePermissions = {
|
||||||
federation: false,
|
|
||||||
settings: false,
|
settings: false,
|
||||||
library: false,
|
library: false,
|
||||||
upload: false
|
moderation: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
profile: (state, value) => {
|
profile: (state, value) => {
|
||||||
|
@ -85,11 +106,15 @@ export default {
|
||||||
authenticated: (state, value) => {
|
authenticated: (state, value) => {
|
||||||
state.authenticated = value
|
state.authenticated = value
|
||||||
if (value === false) {
|
if (value === false) {
|
||||||
state.username = null
|
state.username = ''
|
||||||
state.fullUsername = null
|
state.fullUsername = ''
|
||||||
state.profile = null
|
state.profile = null
|
||||||
state.scopedTokens = getDefaultScopedTokens()
|
state.scopedTokens = getDefaultScopedTokens()
|
||||||
state.availablePermissions = {}
|
state.availablePermissions = {
|
||||||
|
settings: false,
|
||||||
|
library: false,
|
||||||
|
moderation: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
username: (state, value) => {
|
username: (state, value) => {
|
||||||
|
@ -106,13 +131,17 @@ export default {
|
||||||
scopedTokens: (state, value) => {
|
scopedTokens: (state, value) => {
|
||||||
state.scopedTokens = { ...value }
|
state.scopedTokens = { ...value }
|
||||||
},
|
},
|
||||||
permission: (state, { key, status }) => {
|
permission: (state, { key, status }: { key: Permission, status: boolean }) => {
|
||||||
state.availablePermissions[key] = status
|
state.availablePermissions[key] = status
|
||||||
},
|
},
|
||||||
profilePartialUpdate: (state, payload) => {
|
profilePartialUpdate: (state, payload: Profile) => {
|
||||||
Object.keys(payload).forEach((k) => {
|
if (!state.profile) {
|
||||||
state.profile[k] = payload[k]
|
state.profile = {} as Profile
|
||||||
})
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(payload)) {
|
||||||
|
state.profile[key as keyof Profile] = value
|
||||||
|
}
|
||||||
},
|
},
|
||||||
oauthApp: (state, payload) => {
|
oauthApp: (state, payload) => {
|
||||||
state.oauth.clientId = payload.client_id
|
state.oauth.clientId = payload.client_id
|
||||||
|
@ -125,12 +154,9 @@ export default {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
// Send a request to the login URL and save the returned JWT
|
// Send a request to the login URL and save the returned JWT
|
||||||
login ({ commit, dispatch }, { next, credentials, onError }) {
|
login ({ dispatch }, { next, credentials, onError }) {
|
||||||
const form = new FormData()
|
const form = useFormData(credentials)
|
||||||
Object.keys(credentials).forEach((k) => {
|
return axios.post('users/login', form).then(() => {
|
||||||
form.set(k, credentials[k])
|
|
||||||
})
|
|
||||||
return axios.post('users/login', form).then(response => {
|
|
||||||
logger.info('Successfully logged in as', credentials.username)
|
logger.info('Successfully logged in as', credentials.username)
|
||||||
dispatch('fetchProfile').then(() => {
|
dispatch('fetchProfile').then(() => {
|
||||||
// Redirect to a specified route
|
// Redirect to a specified route
|
||||||
|
@ -143,7 +169,7 @@ export default {
|
||||||
onError(response)
|
onError(response)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async logout ({ state, commit }) {
|
async logout ({ commit }) {
|
||||||
try {
|
try {
|
||||||
await axios.post('users/logout')
|
await axios.post('users/logout')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -162,7 +188,7 @@ export default {
|
||||||
})
|
})
|
||||||
logger.info('Log out, goodbye!')
|
logger.info('Log out, goodbye!')
|
||||||
},
|
},
|
||||||
fetchProfile ({ commit, dispatch, state }) {
|
fetchProfile ({ dispatch }) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios.get('users/me/').then((response) => {
|
axios.get('users/me/').then((response) => {
|
||||||
logger.info('Successfully fetched user profile')
|
logger.info('Successfully fetched user profile')
|
||||||
|
@ -181,39 +207,34 @@ export default {
|
||||||
dispatch('moderation/fetchContentFilters', null, { root: true })
|
dispatch('moderation/fetchContentFilters', null, { root: true })
|
||||||
dispatch('playlists/fetchOwn', null, { root: true })
|
dispatch('playlists/fetchOwn', null, { root: true })
|
||||||
resolve(response.data)
|
resolve(response.data)
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.info('Error while fetching user profile')
|
logger.info('Error while fetching user profile')
|
||||||
reject(new Error('Error while fetching user profile'))
|
reject(new Error('Error while fetching user profile'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateProfile ({ commit }, data) {
|
updateProfile ({ commit }, data) {
|
||||||
return new Promise((resolve, reject) => {
|
commit('authenticated', true)
|
||||||
commit('authenticated', true)
|
commit('profile', data)
|
||||||
commit('profile', data)
|
commit('username', data.username)
|
||||||
commit('username', data.username)
|
commit('fullUsername', data.full_username)
|
||||||
commit('fullUsername', data.full_username)
|
if (data.tokens) {
|
||||||
if (data.tokens) {
|
commit('scopedTokens', data.tokens)
|
||||||
commit('scopedTokens', data.tokens)
|
}
|
||||||
}
|
|
||||||
Object.keys(data.permissions).forEach(function (key) {
|
for (const [permission, hasPermission] of Object.entries(data.permissions)) {
|
||||||
// this makes it easier to check for permissions in templates
|
// this makes it easier to check for permissions in templates
|
||||||
commit('permission', {
|
commit('permission', { key: permission, status: hasPermission })
|
||||||
key,
|
}
|
||||||
status: data.permissions[String(key)]
|
|
||||||
})
|
|
||||||
})
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
async oauthLogin ({ state, rootState, commit, getters }, next) {
|
async oauthLogin ({ state, rootState, commit }, next) {
|
||||||
const app = await createOauthApp(getters.appDomain)
|
const app = await createOauthApp()
|
||||||
commit('oauthApp', app)
|
commit('oauthApp', app)
|
||||||
const redirectUri = encodeURIComponent(`${baseUrl}/auth/callback`)
|
const redirectUri = encodeURIComponent(`${location.origin}/auth/callback`)
|
||||||
const params = `response_type=code&scope=${encodeURIComponent(NEEDED_SCOPES)}&redirect_uri=${redirectUri}&state=${next}&client_id=${state.oauth.clientId}`
|
const params = `response_type=code&scope=${encodeURIComponent(NEEDED_SCOPES)}&redirect_uri=${redirectUri}&state=${next}&client_id=${state.oauth.clientId}`
|
||||||
const authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}`
|
const authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}`
|
||||||
console.log('Redirecting user...', authorizeUrl)
|
console.log('Redirecting user...', authorizeUrl)
|
||||||
window.location = authorizeUrl
|
window.location.href = authorizeUrl
|
||||||
},
|
},
|
||||||
async handleOauthCallback ({ state, commit, dispatch }, authorizationCode) {
|
async handleOauthCallback ({ state, commit, dispatch }, authorizationCode) {
|
||||||
console.log('Fetching token...')
|
console.log('Fetching token...')
|
||||||
|
@ -222,17 +243,17 @@ export default {
|
||||||
client_secret: state.oauth.clientSecret,
|
client_secret: state.oauth.clientSecret,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code: authorizationCode,
|
code: authorizationCode,
|
||||||
redirect_uri: `${baseUrl}/auth/callback`
|
redirect_uri: `${location.origin}/auth/callback`
|
||||||
}
|
}
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
'oauth/token/',
|
'oauth/token/',
|
||||||
asForm(payload),
|
useFormData(payload),
|
||||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||||
)
|
)
|
||||||
commit('oauthToken', response.data)
|
commit('oauthToken', response.data)
|
||||||
await dispatch('fetchProfile')
|
await dispatch('fetchProfile')
|
||||||
},
|
},
|
||||||
async refreshOauthToken ({ state, commit }, authorizationCode) {
|
async refreshOauthToken ({ state, commit }) {
|
||||||
const payload = {
|
const payload = {
|
||||||
client_id: state.oauth.clientId,
|
client_id: state.oauth.clientId,
|
||||||
client_secret: state.oauth.clientSecret,
|
client_secret: state.oauth.clientSecret,
|
||||||
|
@ -241,10 +262,12 @@ export default {
|
||||||
}
|
}
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
'oauth/token/',
|
'oauth/token/',
|
||||||
asForm(payload),
|
useFormData(payload),
|
||||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||||
)
|
)
|
||||||
commit('oauthToken', response.data)
|
commit('oauthToken', response.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,9 +1,36 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
subscriptions: string[]
|
||||||
|
count: number
|
||||||
|
showUploadModal: boolean
|
||||||
|
latestPublication: null | Publication
|
||||||
|
uploadModalConfig: {
|
||||||
|
channel: null | Channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Channel {
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Publication {
|
||||||
|
date: Date
|
||||||
|
uploads: Upload[]
|
||||||
|
channel: Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Upload {
|
||||||
|
uuid: string
|
||||||
|
import_status: 'pending' | 'skipped' | 'errored' | 'finished'
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
export default {
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
|
@ -17,19 +44,20 @@ export default {
|
||||||
mutations: {
|
mutations: {
|
||||||
subscriptions: (state, { uuid, value }) => {
|
subscriptions: (state, { uuid, value }) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (state.subscriptions.indexOf(uuid) === -1) {
|
if (!state.subscriptions.includes(uuid)) {
|
||||||
state.subscriptions.push(uuid)
|
state.subscriptions.push(uuid)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const i = state.subscriptions.indexOf(uuid)
|
const index = state.subscriptions.indexOf(uuid)
|
||||||
if (i > -1) {
|
if (index > -1) {
|
||||||
state.subscriptions.splice(i, 1)
|
state.subscriptions.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.count = state.subscriptions.length
|
state.count = state.subscriptions.length
|
||||||
},
|
},
|
||||||
reset (state) {
|
reset (state) {
|
||||||
state.subscriptions = []
|
state.subscriptions.length = 0
|
||||||
state.count = 0
|
state.count = 0
|
||||||
},
|
},
|
||||||
showUploadModal (state, value) {
|
showUploadModal (state, value) {
|
||||||
|
@ -50,24 +78,22 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
isSubscribed: (state) => (uuid) => {
|
isSubscribed: (state) => (uuid: string) => state.subscriptions.includes(uuid)
|
||||||
return state.subscriptions.indexOf(uuid) > -1
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
set ({ commit, state }, { uuid, value }) {
|
set ({ commit }, { uuid, value }) {
|
||||||
commit('subscriptions', { uuid, value })
|
commit('subscriptions', { uuid, value })
|
||||||
if (value) {
|
if (value) {
|
||||||
return axios.post(`channels/${uuid}/subscribe/`).then((response) => {
|
return axios.post(`channels/${uuid}/subscribe/`).then(() => {
|
||||||
logger.info('Successfully subscribed to channel')
|
logger.info('Successfully subscribed to channel')
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.info('Error while subscribing to channel')
|
logger.info('Error while subscribing to channel')
|
||||||
commit('subscriptions', { uuid, value: !value })
|
commit('subscriptions', { uuid, value: !value })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return axios.post(`channels/${uuid}/unsubscribe/`).then((response) => {
|
return axios.post(`channels/${uuid}/unsubscribe/`).then(() => {
|
||||||
logger.info('Successfully unsubscribed from channel')
|
logger.info('Successfully unsubscribed from channel')
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.info('Error while unsubscribing from channel')
|
logger.info('Error while unsubscribing from channel')
|
||||||
commit('subscriptions', { uuid, value: !value })
|
commit('subscriptions', { uuid, value: !value })
|
||||||
})
|
})
|
||||||
|
@ -76,13 +102,15 @@ export default {
|
||||||
toggle ({ getters, dispatch }, uuid) {
|
toggle ({ getters, dispatch }, uuid) {
|
||||||
dispatch('set', { uuid, value: !getters.isSubscribed(uuid) })
|
dispatch('set', { uuid, value: !getters.isSubscribed(uuid) })
|
||||||
},
|
},
|
||||||
fetchSubscriptions ({ dispatch, state, commit, rootState }, url) {
|
fetchSubscriptions ({ commit }) {
|
||||||
const promise = axios.get('subscriptions/all/')
|
const promise = axios.get('subscriptions/all/')
|
||||||
return promise.then((response) => {
|
return promise.then((response) => {
|
||||||
response.data.results.forEach(result => {
|
response.data.results.forEach((result: { channel: unknown }) => {
|
||||||
commit('subscriptions', { uuid: result.channel, value: true })
|
commit('subscriptions', { uuid: result.channel, value: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
tracks: string[]
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
export default {
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
@ -12,41 +19,42 @@ export default {
|
||||||
mutations: {
|
mutations: {
|
||||||
track: (state, { id, value }) => {
|
track: (state, { id, value }) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
if (state.tracks.indexOf(id) === -1) {
|
if (!state.tracks.includes(id)) {
|
||||||
state.tracks.push(id)
|
state.tracks.push(id)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const i = state.tracks.indexOf(id)
|
const index = state.tracks.indexOf(id)
|
||||||
if (i > -1) {
|
if (index > -1) {
|
||||||
state.tracks.splice(i, 1)
|
state.tracks.splice(index, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.count = state.tracks.length
|
state.count = state.tracks.length
|
||||||
},
|
},
|
||||||
reset (state) {
|
reset (state) {
|
||||||
state.tracks = []
|
state.tracks.length = 0
|
||||||
state.count = 0
|
state.count = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
isFavorite: (state) => (id) => {
|
isFavorite: (state) => (id: string) => {
|
||||||
return state.tracks.indexOf(id) > -1
|
return state.tracks.includes(id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
set ({ commit, state }, { id, value }) {
|
set ({ commit }, { id, value }) {
|
||||||
commit('track', { id, value })
|
commit('track', { id, value })
|
||||||
if (value) {
|
if (value) {
|
||||||
return axios.post('favorites/tracks/', { track: id }).then((response) => {
|
return axios.post('favorites/tracks/', { track: id }).then(() => {
|
||||||
logger.info('Successfully added track to favorites')
|
logger.info('Successfully added track to favorites')
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.info('Error while adding track to favorites')
|
logger.info('Error while adding track to favorites')
|
||||||
commit('track', { id, value: !value })
|
commit('track', { id, value: !value })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return axios.post('favorites/tracks/remove/', { track: id }).then((response) => {
|
return axios.post('favorites/tracks/remove/', { track: id }).then(() => {
|
||||||
logger.info('Successfully removed track from favorites')
|
logger.info('Successfully removed track from favorites')
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.info('Error while removing track from favorites')
|
logger.info('Error while removing track from favorites')
|
||||||
commit('track', { id, value: !value })
|
commit('track', { id, value: !value })
|
||||||
})
|
})
|
||||||
|
@ -55,20 +63,22 @@ export default {
|
||||||
toggle ({ getters, dispatch }, id) {
|
toggle ({ getters, dispatch }, id) {
|
||||||
dispatch('set', { id, value: !getters.isFavorite(id) })
|
dispatch('set', { id, value: !getters.isFavorite(id) })
|
||||||
},
|
},
|
||||||
fetch ({ dispatch, state, commit, rootState }, url) {
|
fetch ({ commit, rootState }) {
|
||||||
// will fetch favorites by batches from API to have them locally
|
// will fetch favorites by batches from API to have them locally
|
||||||
const params = {
|
const params = {
|
||||||
user: rootState.auth.profile.id,
|
user: rootState.auth.profile?.id,
|
||||||
page_size: 50,
|
page_size: 50,
|
||||||
ordering: '-creation_date'
|
ordering: '-creation_date'
|
||||||
}
|
}
|
||||||
const promise = axios.get('favorites/tracks/all/', { params: params })
|
const promise = axios.get('favorites/tracks/all/', { params: params })
|
||||||
return promise.then((response) => {
|
return promise.then((response) => {
|
||||||
logger.info('Fetched a batch of ' + response.data.results.length + ' favorites')
|
logger.info('Fetched a batch of ' + response.data.results.length + ' favorites')
|
||||||
response.data.results.forEach(result => {
|
response.data.results.forEach((result: { track: string }) => {
|
||||||
commit('track', { id: result.track, value: true })
|
commit('track', { id: result.track, value: true })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,21 +1,31 @@
|
||||||
import { createStore, Store } from 'vuex'
|
import { createStore, Store, useStore as baseUseStore } from 'vuex'
|
||||||
import createPersistedState from 'vuex-persistedstate'
|
import createPersistedState from 'vuex-persistedstate'
|
||||||
|
|
||||||
import favorites from './favorites'
|
import favorites, { State as FavoritesState } from './favorites'
|
||||||
import channels from './channels'
|
import channels, { State as ChannelsState } from './channels'
|
||||||
import libraries from './libraries'
|
import libraries, { State as LibrariesState } from './libraries'
|
||||||
import auth from './auth'
|
import auth, { State as AuthState } from './auth'
|
||||||
import instance from './instance'
|
import instance, { State as InstanceState } from './instance'
|
||||||
import moderation from './moderation'
|
import moderation, { State as ModerationState } from './moderation'
|
||||||
import queue from './queue'
|
import queue, { State as QueueState } from './queue'
|
||||||
import radios from './radios'
|
import radios, { State as RadiosState } from './radios'
|
||||||
import player from './player'
|
import player, { State as PlayerState } from './player'
|
||||||
import playlists from './playlists'
|
import playlists, { State as PlaylistsState } from './playlists'
|
||||||
import ui from './ui'
|
import ui, { State as UiState } from './ui'
|
||||||
import { InjectionKey } from 'vue'
|
import { InjectionKey } from 'vue'
|
||||||
|
|
||||||
export interface RootState {
|
export interface RootState {
|
||||||
|
ui: UiState
|
||||||
|
auth: AuthState
|
||||||
|
channels: ChannelsState
|
||||||
|
libraries: LibrariesState
|
||||||
|
favorites: FavoritesState
|
||||||
|
instance: InstanceState
|
||||||
|
moderation: ModerationState
|
||||||
|
queue: QueueState
|
||||||
|
radios: RadiosState
|
||||||
|
playlists: PlaylistsState
|
||||||
|
player: PlayerState
|
||||||
}
|
}
|
||||||
|
|
||||||
export const key: InjectionKey<Store<RootState>> = Symbol('vuex state injection key')
|
export const key: InjectionKey<Store<RootState>> = Symbol('vuex state injection key')
|
||||||
|
@ -43,7 +53,7 @@ export default createStore<RootState>({
|
||||||
}),
|
}),
|
||||||
createPersistedState({
|
createPersistedState({
|
||||||
key: 'instance',
|
key: 'instance',
|
||||||
paths: ['instance.events', 'instance.instanceUrl', 'instance.knownInstances']
|
paths: ['instance.instanceUrl', 'instance.knownInstances']
|
||||||
}),
|
}),
|
||||||
createPersistedState({
|
createPersistedState({
|
||||||
key: 'ui',
|
key: 'ui',
|
||||||
|
@ -109,3 +119,7 @@ export default createStore<RootState>({
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const useStore = () => {
|
||||||
|
return baseUseStore(key)
|
||||||
|
}
|
||||||
|
|
|
@ -1,23 +1,59 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { merge } from 'lodash-es'
|
import { merge } from 'lodash-es'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
frontSettings: {
|
||||||
|
defaultServerUrl: string
|
||||||
|
additionalStylesheets: string[] // TODO (wvffle): Ensure it's not nullable
|
||||||
|
}
|
||||||
|
instanceUrl: string
|
||||||
|
knownInstances: string[]
|
||||||
|
nodeinfo: unknown | null // TODO (wvffle): Get nodeinfo type from swagger automatically
|
||||||
|
settings: Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InstanceSettings {
|
||||||
|
name: { value: string }
|
||||||
|
short_description: { value: string }
|
||||||
|
long_description: { value: string }
|
||||||
|
funkwhale_support_message_enabled: { value: boolean }
|
||||||
|
support_message: { value: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UsersSettings {
|
||||||
|
registration_enabled: { value: boolean }
|
||||||
|
upload_quota: { value: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModerationSettings {
|
||||||
|
signup_approval_enabled: { value: boolean }
|
||||||
|
signup_form_customization: { value: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubsonicSettings {
|
||||||
|
enabled: { value: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Settings {
|
||||||
|
instance: InstanceSettings
|
||||||
|
users: UsersSettings
|
||||||
|
moderation: ModerationSettings
|
||||||
|
subsonic: SubsonicSettings
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
function getDefaultUrl () {
|
const store: Module<State, RootState> = {
|
||||||
return (
|
|
||||||
window.location.protocol + '//' + window.location.hostname +
|
|
||||||
(window.location.port ? ':' + window.location.port : '') + '/'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
maxEvents: 200,
|
frontSettings: {
|
||||||
frontSettings: {},
|
defaultServerUrl: location.origin,
|
||||||
instanceUrl: import.meta.env.VUE_APP_INSTANCE_URL,
|
additionalStylesheets: []
|
||||||
events: [],
|
},
|
||||||
|
instanceUrl: import.meta.env.VUE_APP_INSTANCE_URL as string,
|
||||||
knownInstances: [],
|
knownInstances: [],
|
||||||
nodeinfo: null,
|
nodeinfo: null,
|
||||||
settings: {
|
settings: {
|
||||||
|
@ -63,15 +99,6 @@ export default {
|
||||||
settings: (state, value) => {
|
settings: (state, value) => {
|
||||||
merge(state.settings, value)
|
merge(state.settings, value)
|
||||||
},
|
},
|
||||||
event: (state, value) => {
|
|
||||||
state.events.unshift(value)
|
|
||||||
if (state.events.length > state.maxEvents) {
|
|
||||||
state.events = state.events.slice(0, state.maxEvents)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
events: (state, value) => {
|
|
||||||
state.events = value
|
|
||||||
},
|
|
||||||
nodeinfo: (state, value) => {
|
nodeinfo: (state, value) => {
|
||||||
state.nodeinfo = value
|
state.nodeinfo = value
|
||||||
},
|
},
|
||||||
|
@ -82,6 +109,7 @@ export default {
|
||||||
if (value && !value.endsWith('/')) {
|
if (value && !value.endsWith('/')) {
|
||||||
value = value + '/'
|
value = value + '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
state.instanceUrl = value
|
state.instanceUrl = value
|
||||||
|
|
||||||
// append the URL to the list (and remove existing one if needed)
|
// append the URL to the list (and remove existing one if needed)
|
||||||
|
@ -92,8 +120,9 @@ export default {
|
||||||
}
|
}
|
||||||
state.knownInstances.splice(0, 0, value)
|
state.knownInstances.splice(0, 0, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
axios.defaults.baseURL = null
|
axios.defaults.baseURL = undefined
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const suffix = 'api/v1/'
|
const suffix = 'api/v1/'
|
||||||
|
@ -101,18 +130,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
defaultUrl: (state) => () => {
|
absoluteUrl: (state) => (relativeUrl: string) => {
|
||||||
return getDefaultUrl()
|
if (relativeUrl.startsWith('http')) return relativeUrl
|
||||||
},
|
if (state.instanceUrl.endsWith('/') && relativeUrl.startsWith('/')) {
|
||||||
absoluteUrl: (state) => (relativeUrl) => {
|
|
||||||
if (relativeUrl.startsWith('http')) {
|
|
||||||
return relativeUrl
|
|
||||||
}
|
|
||||||
if (state.instanceUrl?.endsWith('/') && relativeUrl.startsWith('/')) {
|
|
||||||
relativeUrl = relativeUrl.slice(1)
|
relativeUrl = relativeUrl.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceUrl = state.instanceUrl ?? getDefaultUrl()
|
const instanceUrl = state.instanceUrl ?? location.origin
|
||||||
return instanceUrl + relativeUrl
|
return instanceUrl + relativeUrl
|
||||||
},
|
},
|
||||||
domain: (state) => {
|
domain: (state) => {
|
||||||
|
@ -121,12 +145,10 @@ export default {
|
||||||
parser.href = url
|
parser.href = url
|
||||||
return parser.hostname
|
return parser.hostname
|
||||||
},
|
},
|
||||||
appDomain: (state) => {
|
appDomain: () => location.hostname
|
||||||
return location.hostname
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setUrl ({ commit, dispatch }, url) {
|
setUrl ({ commit }, url) {
|
||||||
commit('instanceUrl', url)
|
commit('instanceUrl', url)
|
||||||
const modules = [
|
const modules = [
|
||||||
'auth',
|
'auth',
|
||||||
|
@ -146,7 +168,8 @@ export default {
|
||||||
return axios.get('instance/settings/').then(response => {
|
return axios.get('instance/settings/').then(response => {
|
||||||
logger.info('Successfully fetched instance settings')
|
logger.info('Successfully fetched instance settings')
|
||||||
|
|
||||||
const sections = response.data.reduce((map, entry) => {
|
type SettingsSection = { section: string, name: string }
|
||||||
|
const sections = response.data.reduce((map: Record<string, Record<string, SettingsSection>>, entry: SettingsSection) => {
|
||||||
map[entry.section] ??= {}
|
map[entry.section] ??= {}
|
||||||
map[entry.section][entry.name] = entry
|
map[entry.section][entry.name] = entry
|
||||||
return map
|
return map
|
||||||
|
@ -161,9 +184,11 @@ export default {
|
||||||
fetchFrontSettings ({ commit }) {
|
fetchFrontSettings ({ commit }) {
|
||||||
return axios.get('/settings.json').then(response => {
|
return axios.get('/settings.json').then(response => {
|
||||||
commit('frontSettings', response.data)
|
commit('frontSettings', response.data)
|
||||||
}, response => {
|
}, () => {
|
||||||
logger.error('Error when fetching front-end configuration (or no customization available)')
|
logger.error('Error when fetching front-end configuration (or no customization available)')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,41 +1,44 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
followsByLibrary: {
|
||||||
|
[key: string]: Library
|
||||||
|
}
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Library {
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
export default {
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
followedLibraries: [],
|
|
||||||
followsByLibrary: {},
|
followsByLibrary: {},
|
||||||
count: 0
|
count: 0
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
follows: (state, { library, follow }) => {
|
follows: (state, { library, follow }) => {
|
||||||
const replacement = { ...state.followsByLibrary }
|
|
||||||
if (follow) {
|
if (follow) {
|
||||||
if (state.followedLibraries.indexOf(library) === -1) {
|
state.followsByLibrary[library] = follow
|
||||||
state.followedLibraries.push(library)
|
|
||||||
replacement[library] = follow
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const i = state.followedLibraries.indexOf(library)
|
delete state.followsByLibrary[library]
|
||||||
if (i > -1) {
|
|
||||||
state.followedLibraries.splice(i, 1)
|
|
||||||
replacement[library] = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
state.followsByLibrary = replacement
|
|
||||||
state.count = state.followedLibraries.length
|
state.count = Object.keys(state.followsByLibrary).length
|
||||||
},
|
},
|
||||||
reset (state) {
|
reset (state) {
|
||||||
state.followedLibraries = []
|
|
||||||
state.followsByLibrary = {}
|
state.followsByLibrary = {}
|
||||||
state.count = 0
|
state.count = 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
follow: (state) => (library) => {
|
follow: (state) => (library: string) => {
|
||||||
return state.followsByLibrary[library]
|
return state.followsByLibrary[library]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -45,16 +48,16 @@ export default {
|
||||||
return axios.post('federation/follows/library/', { target: uuid }).then((response) => {
|
return axios.post('federation/follows/library/', { target: uuid }).then((response) => {
|
||||||
logger.info('Successfully subscribed to library')
|
logger.info('Successfully subscribed to library')
|
||||||
commit('follows', { library: uuid, follow: response.data })
|
commit('follows', { library: uuid, follow: response.data })
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.info('Error while subscribing to library')
|
logger.info('Error while subscribing to library')
|
||||||
commit('follows', { library: uuid, follow: null })
|
commit('follows', { library: uuid, follow: null })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
const follow = state.followsByLibrary[uuid]
|
const follow = state.followsByLibrary[uuid]
|
||||||
return axios.delete(`federation/follows/library/${follow.uuid}/`).then((response) => {
|
return axios.delete(`federation/follows/library/${follow.uuid}/`).then(() => {
|
||||||
logger.info('Successfully unsubscribed from library')
|
logger.info('Successfully unsubscribed from library')
|
||||||
commit('follows', { library: uuid, follow: null })
|
commit('follows', { library: uuid, follow: null })
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.info('Error while unsubscribing from library')
|
logger.info('Error while unsubscribing from library')
|
||||||
commit('follows', { library: uuid, follow: follow })
|
commit('follows', { library: uuid, follow: follow })
|
||||||
})
|
})
|
||||||
|
@ -66,10 +69,12 @@ export default {
|
||||||
fetchFollows ({ dispatch, state, commit, rootState }, url) {
|
fetchFollows ({ dispatch, state, commit, rootState }, url) {
|
||||||
const promise = axios.get('federation/follows/library/all/')
|
const promise = axios.get('federation/follows/library/all/')
|
||||||
return promise.then((response) => {
|
return promise.then((response) => {
|
||||||
response.data.results.forEach(result => {
|
response.data.results.forEach((result: { library: string }) => {
|
||||||
commit('follows', { library: result.library, follow: result })
|
commit('follows', { library: result.library, follow: result })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,10 +1,35 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { sortBy } from 'lodash-es'
|
import { sortBy } from 'lodash-es'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
filters: ContentFilter[],
|
||||||
|
showFilterModal: boolean,
|
||||||
|
showReportModal: boolean,
|
||||||
|
lastUpdate: Date,
|
||||||
|
filterModalTarget: {
|
||||||
|
type: null,
|
||||||
|
target: null
|
||||||
|
},
|
||||||
|
reportModalTarget: {
|
||||||
|
type: null,
|
||||||
|
target: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContentFilter {
|
||||||
|
uuid: string
|
||||||
|
creation_date: Date
|
||||||
|
target: {
|
||||||
|
type: 'artist'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
export default {
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
filters: [],
|
filters: [],
|
||||||
|
@ -56,10 +81,16 @@ export default {
|
||||||
},
|
},
|
||||||
reset (state) {
|
reset (state) {
|
||||||
state.filters = []
|
state.filters = []
|
||||||
state.filterModalTarget = null
|
state.filterModalTarget = {
|
||||||
|
type: null,
|
||||||
|
target: null
|
||||||
|
}
|
||||||
state.showFilterModal = false
|
state.showFilterModal = false
|
||||||
state.showReportModal = false
|
state.showReportModal = false
|
||||||
state.reportModalTarget = {}
|
state.reportModalTarget = {
|
||||||
|
type: null,
|
||||||
|
target: null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
deleteContentFilter (state, uuid) {
|
deleteContentFilter (state, uuid) {
|
||||||
state.filters = state.filters.filter((e) => {
|
state.filters = state.filters.filter((e) => {
|
||||||
|
@ -86,7 +117,7 @@ export default {
|
||||||
commit('reportModalTarget', payload)
|
commit('reportModalTarget', payload)
|
||||||
commit('showReportModal', true)
|
commit('showReportModal', true)
|
||||||
},
|
},
|
||||||
fetchContentFilters ({ dispatch, state, commit, rootState }, url) {
|
fetchContentFilters ({ dispatch, commit }, url) {
|
||||||
let params = {}
|
let params = {}
|
||||||
let promise
|
let promise
|
||||||
if (url) {
|
if (url) {
|
||||||
|
@ -104,15 +135,17 @@ export default {
|
||||||
if (response.data.next) {
|
if (response.data.next) {
|
||||||
dispatch('fetchContentFilters', response.data.next)
|
dispatch('fetchContentFilters', response.data.next)
|
||||||
}
|
}
|
||||||
response.data.results.forEach(result => {
|
response.data.results.forEach((result: ContentFilter) => {
|
||||||
commit('contentFilter', result)
|
commit('contentFilter', result)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteContentFilter ({ commit }, uuid) {
|
deleteContentFilter ({ commit }, uuid) {
|
||||||
return axios.delete(`moderation/content-filters/${uuid}/`).then((response) => {
|
return axios.delete(`moderation/content-filters/${uuid}/`).then(() => {
|
||||||
commit('deleteContentFilter', uuid)
|
commit('deleteContentFilter', uuid)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,10 +1,26 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import time from '~/utils/time'
|
import time from '~/utils/time'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
maxConsecutiveErrors: number
|
||||||
|
errorCount: number
|
||||||
|
playing: boolean
|
||||||
|
isLoadingAudio: boolean
|
||||||
|
volume: number
|
||||||
|
tempVolume: number
|
||||||
|
duration: number
|
||||||
|
currentTime: number
|
||||||
|
errored: boolean
|
||||||
|
bufferProgress: number
|
||||||
|
looping: 0 | 1 | 2
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
export default {
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
maxConsecutiveErrors: 5,
|
maxConsecutiveErrors: 5,
|
||||||
|
@ -79,12 +95,11 @@ export default {
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
durationFormatted: state => {
|
durationFormatted: state => {
|
||||||
let duration = parseInt(state.duration)
|
if (state.duration % 1 !== 0) {
|
||||||
if (duration % 1 !== 0) {
|
|
||||||
return time.parse(0)
|
return time.parse(0)
|
||||||
}
|
}
|
||||||
duration = Math.round(state.duration)
|
|
||||||
return time.parse(duration)
|
return time.parse(Math.round(state.duration))
|
||||||
},
|
},
|
||||||
currentTimeFormatted: state => {
|
currentTimeFormatted: state => {
|
||||||
return time.parse(Math.round(state.currentTime))
|
return time.parse(Math.round(state.currentTime))
|
||||||
|
@ -132,15 +147,15 @@ export default {
|
||||||
commit('volume', state.tempVolume)
|
commit('volume', state.tempVolume)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
trackListened ({ commit, rootState }, track) {
|
trackListened ({ rootState }, track) {
|
||||||
if (!rootState.auth.authenticated) {
|
if (!rootState.auth.authenticated) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return axios.post('history/listenings/', { track: track.id }).then((response) => {}, (response) => {
|
return axios.post('history/listenings/', { track: track.id }).then(() => {}, () => {
|
||||||
logger.error('Could not record track in history')
|
logger.error('Could not record track in history')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
trackEnded ({ commit, dispatch, rootState }, track) {
|
trackEnded ({ commit, dispatch, rootState }) {
|
||||||
const queueState = rootState.queue
|
const queueState = rootState.queue
|
||||||
if (queueState.currentIndex === queueState.tracks.length - 1) {
|
if (queueState.currentIndex === queueState.tracks.length - 1) {
|
||||||
// we've reached last track of queue, trigger a reload
|
// we've reached last track of queue, trigger a reload
|
||||||
|
@ -177,3 +192,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
export default {
|
export interface State {
|
||||||
|
playlists: any[]
|
||||||
|
showModal: boolean
|
||||||
|
modalTrack: null
|
||||||
|
}
|
||||||
|
|
||||||
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
playlists: [],
|
playlists: [],
|
||||||
|
@ -26,18 +34,20 @@ export default {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async fetchOwn ({ commit, rootState }) {
|
async fetchOwn ({ commit, rootState }) {
|
||||||
const userId = rootState.auth.profile.id
|
const userId = rootState.auth.profile?.id
|
||||||
if (!userId) {
|
if (!userId) return
|
||||||
return
|
|
||||||
}
|
const playlists = []
|
||||||
let playlists = []
|
|
||||||
let url = 'playlists/'
|
let url = 'playlists/'
|
||||||
while (url != null) {
|
while (url != null) {
|
||||||
const response = await axios.get(url, { params: { scope: 'me' } })
|
const response = await axios.get(url, { params: { scope: 'me' } })
|
||||||
playlists = [...playlists, ...response.data.results]
|
playlists.push(...response.data.results)
|
||||||
url = response.data.next
|
url = response.data.next
|
||||||
}
|
}
|
||||||
|
|
||||||
commit('playlists', playlists)
|
commit('playlists', playlists)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,9 +1,18 @@
|
||||||
import { shuffle } from 'lodash-es'
|
import { shuffle } from 'lodash-es'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
import { Track } from '~/types'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
tracks: Track[]
|
||||||
|
currentIndex: number
|
||||||
|
ended: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
export default {
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
tracks: [],
|
tracks: [],
|
||||||
|
@ -12,7 +21,7 @@ export default {
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
reset (state) {
|
reset (state) {
|
||||||
state.tracks = []
|
state.tracks.length = 0
|
||||||
state.currentIndex = -1
|
state.currentIndex = -1
|
||||||
state.ended = true
|
state.ended = true
|
||||||
},
|
},
|
||||||
|
@ -62,7 +71,7 @@ export default {
|
||||||
isEmpty: state => state.tracks.length === 0
|
isEmpty: state => state.tracks.length === 0
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
append ({ commit, state, dispatch }, { track, index }) {
|
append ({ commit, state }, { track, index }) {
|
||||||
index = index || state.tracks.length
|
index = index || state.tracks.length
|
||||||
if (index > state.tracks.length - 1) {
|
if (index > state.tracks.length - 1) {
|
||||||
// we simply push to the end
|
// we simply push to the end
|
||||||
|
@ -73,26 +82,28 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
appendMany ({ state, commit, dispatch }, { tracks, index, callback }) {
|
appendMany ({ state, dispatch }, { tracks, index, callback }) {
|
||||||
logger.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
|
logger.info('Appending many tracks to the queue', tracks.map((track: Track) => track.title))
|
||||||
let shouldPlay = false
|
let shouldPlay = false
|
||||||
|
|
||||||
if (state.tracks.length === 0) {
|
if (state.tracks.length === 0) {
|
||||||
index = 0
|
index = 0
|
||||||
shouldPlay = true
|
shouldPlay = true
|
||||||
} else {
|
} else {
|
||||||
index = index || state.tracks.length
|
index = index ?? state.tracks.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = tracks.length
|
const total = tracks.length
|
||||||
tracks.forEach((t, i) => {
|
tracks.forEach((track: Track, i: number) => {
|
||||||
const p = dispatch('append', { track: t, index: index })
|
const promise = dispatch('append', { track: track, index: index })
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
if (callback && i + 1 === total) {
|
if (callback && i + 1 === total) {
|
||||||
p.then(callback)
|
promise.then(callback)
|
||||||
}
|
}
|
||||||
if (shouldPlay && p && i + 1 === total) {
|
|
||||||
p.then(() => {
|
if (shouldPlay && promise && i + 1 === total) {
|
||||||
dispatch('next')
|
promise.then(() => dispatch('next'))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -163,7 +174,7 @@ export default {
|
||||||
async shuffle ({ dispatch, state }, callback) {
|
async shuffle ({ dispatch, state }, callback) {
|
||||||
const shuffled = shuffle(state.tracks)
|
const shuffled = shuffle(state.tracks)
|
||||||
state.tracks.length = 0
|
state.tracks.length = 0
|
||||||
const params = { tracks: shuffled }
|
const params: { tracks: Track[], callback?: () => unknown } = { tracks: shuffled }
|
||||||
if (callback) {
|
if (callback) {
|
||||||
params.callback = callback
|
params.callback = callback
|
||||||
}
|
}
|
||||||
|
@ -172,3 +183,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,17 +1,40 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { getClientOnlyRadio } from '~/radios'
|
import { CLIENT_RADIOS } from '~/utils/clientRadios'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { Dispatch, Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
current: null | CurrentRadio
|
||||||
|
running: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CurrentRadio {
|
||||||
|
clientOnly: boolean
|
||||||
|
session: null
|
||||||
|
type: 'account'
|
||||||
|
objectId: {
|
||||||
|
username: string
|
||||||
|
fullUsername: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PopulateQueuePayload {
|
||||||
|
current: CurrentRadio
|
||||||
|
playNow: boolean
|
||||||
|
dispatch: Dispatch
|
||||||
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
export default {
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
current: null,
|
current: null,
|
||||||
running: false
|
running: false
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
types: state => {
|
types: () => {
|
||||||
return {
|
return {
|
||||||
'actor-content': {
|
'actor-content': {
|
||||||
name: 'Your content',
|
name: 'Your content',
|
||||||
|
@ -39,7 +62,7 @@ export default {
|
||||||
mutations: {
|
mutations: {
|
||||||
reset (state) {
|
reset (state) {
|
||||||
state.running = false
|
state.running = false
|
||||||
state.current = false
|
state.current = null
|
||||||
},
|
},
|
||||||
current: (state, value) => {
|
current: (state, value) => {
|
||||||
state.current = value
|
state.current = value
|
||||||
|
@ -67,13 +90,13 @@ export default {
|
||||||
commit('current', { type, objectId, session: response.data.id, customRadioId })
|
commit('current', { type, objectId, session: response.data.id, customRadioId })
|
||||||
commit('running', true)
|
commit('running', true)
|
||||||
dispatch('populateQueue', true)
|
dispatch('populateQueue', true)
|
||||||
}, (response) => {
|
}, () => {
|
||||||
logger.error('Error while starting radio', type)
|
logger.error('Error while starting radio', type)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
stop ({ commit, state }) {
|
stop ({ commit, state }) {
|
||||||
if (state.current && state.current.clientOnly) {
|
if (state.current?.clientOnly) {
|
||||||
getClientOnlyRadio(state.current).stop()
|
CLIENT_RADIOS[state.current.type].stop()
|
||||||
}
|
}
|
||||||
commit('current', null)
|
commit('current', null)
|
||||||
commit('running', false)
|
commit('running', false)
|
||||||
|
@ -82,15 +105,17 @@ export default {
|
||||||
if (!state.running) {
|
if (!state.running) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rootState.player.errorCount >= rootState.player.maxConsecutiveErrors - 1) {
|
if (rootState.player.errorCount >= rootState.player.maxConsecutiveErrors - 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const params = {
|
|
||||||
session: state.current.session
|
const params = { session: state.current?.session }
|
||||||
}
|
|
||||||
if (state.current.clientOnly) {
|
if (state.current?.clientOnly) {
|
||||||
return getClientOnlyRadio(state.current).populateQueue({ current: state.current, dispatch, state, rootState, playNow })
|
return CLIENT_RADIOS[state.current.type].populateQueue({ current: state.current, dispatch, playNow })
|
||||||
}
|
}
|
||||||
|
|
||||||
return axios.post('radios/tracks/', params).then((response) => {
|
return axios.post('radios/tracks/', params).then((response) => {
|
||||||
logger.info('Adding track to queue from radio')
|
logger.info('Adding track to queue from radio')
|
||||||
const append = dispatch('queue/append', { track: response.data.track }, { root: true })
|
const append = dispatch('queue/append', { track: response.data.track }, { root: true })
|
||||||
|
@ -107,3 +132,5 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,7 +1,66 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import { Module } from 'vuex'
|
||||||
|
import { RootState } from '~/store/index'
|
||||||
|
import { availableLanguages } from '~/init/locale'
|
||||||
|
|
||||||
export default {
|
type SupportedExtension = 'flac' | 'ogg' | 'mp3' | 'opus' | 'aac' | 'm4a' | 'aiff' | 'aif'
|
||||||
|
|
||||||
|
type RouteWithPreferences = 'library.artists.browse' | 'library.podcasts.browse' | 'library.radios.browse'
|
||||||
|
| 'library.playlists.browse' | 'library.albums.me' | 'library.artists.me' | 'library.radios.me'
|
||||||
|
| 'library.playlists.me' | 'content.libraries.files' | 'library.detail.upload' | 'library.detail.edit'
|
||||||
|
| 'library.detail' | 'favorites' | 'manage.channels' | 'manage.library.tags' | 'manage.library.uploads'
|
||||||
|
| 'manage.library.libraries' | 'manage.library.tracks' | 'manage.library.albums' | 'manage.library.artists'
|
||||||
|
| 'manage.library.edits' | 'manage.users.users.list' | 'manage.users.invitations.list'
|
||||||
|
| 'manage.moderation.accounts.list' | 'manage.moderation.domains.list' | 'manage.moderation.requests.list'
|
||||||
|
| 'manage.moderation.reports.list' | 'library.albums.browse'
|
||||||
|
|
||||||
|
export type WebSocketEventName = 'inbox.item_added' | 'import.status_updated' | 'mutation.created' | 'mutation.updated'
|
||||||
|
| 'report.created' | 'user_request.created' | 'Listen'
|
||||||
|
|
||||||
|
type Ordering = 'creation_date'
|
||||||
|
type OrderingDirection = '-'
|
||||||
|
interface RoutePreferences {
|
||||||
|
paginateBy: number
|
||||||
|
orderingDirection: OrderingDirection
|
||||||
|
ordering: Ordering
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebSocketEvent {
|
||||||
|
type: WebSocketEventName
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebSocketHandlers = Record<string, (event: WebSocketEvent) => void>
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
displayTime: number
|
||||||
|
key: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationsKey = 'inbox' | 'pendingReviewEdits' | 'pendingReviewReports' | 'pendingReviewRequests'
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
currentLanguage: 'en_US' | keyof typeof availableLanguages
|
||||||
|
selectedLanguage: boolean
|
||||||
|
queueFocused: null
|
||||||
|
momentLocale: 'en'
|
||||||
|
lastDate: Date
|
||||||
|
maxMessages: number
|
||||||
|
messageDisplayDuration: number
|
||||||
|
supportedExtensions: SupportedExtension[]
|
||||||
|
messages: Message[]
|
||||||
|
window: {
|
||||||
|
height: number
|
||||||
|
width: number
|
||||||
|
}
|
||||||
|
pageTitle: null
|
||||||
|
|
||||||
|
notifications: Record<NotificationsKey, number>
|
||||||
|
websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers>
|
||||||
|
routePreferences: Record<RouteWithPreferences, RoutePreferences>
|
||||||
|
}
|
||||||
|
|
||||||
|
const store: Module<State, RootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
currentLanguage: 'en_US',
|
currentLanguage: 'en_US',
|
||||||
|
@ -214,7 +273,7 @@ export default {
|
||||||
return count
|
return count
|
||||||
},
|
},
|
||||||
|
|
||||||
windowSize: (state, getters) => {
|
windowSize: (state) => {
|
||||||
// IMPORTANT: if you modify these breakpoints, also modify the values in
|
// IMPORTANT: if you modify these breakpoints, also modify the values in
|
||||||
// style/vendor/_media.scss
|
// style/vendor/_media.scss
|
||||||
const width = state.window.width
|
const width = state.window.width
|
||||||
|
@ -241,10 +300,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
addWebsocketEventHandler: (state, { eventName, id, handler }) => {
|
addWebsocketEventHandler: (state, { eventName, id, handler }: { eventName: WebSocketEventName, id: string, handler: () => void}) => {
|
||||||
state.websocketEventsHandlers[eventName][id] = handler
|
state.websocketEventsHandlers[eventName][id] = handler
|
||||||
},
|
},
|
||||||
removeWebsocketEventHandler: (state, { eventName, id }) => {
|
removeWebsocketEventHandler: (state, { eventName, id }: { eventName: WebSocketEventName, id: string }) => {
|
||||||
delete state.websocketEventsHandlers[eventName][id]
|
delete state.websocketEventsHandlers[eventName][id]
|
||||||
},
|
},
|
||||||
currentLanguage: (state, value) => {
|
currentLanguage: (state, value) => {
|
||||||
|
@ -279,10 +338,10 @@ export default {
|
||||||
removeMessage (state, key) {
|
removeMessage (state, key) {
|
||||||
state.messages.splice(state.messages.findIndex(message => message.key === key), 1)
|
state.messages.splice(state.messages.findIndex(message => message.key === key), 1)
|
||||||
},
|
},
|
||||||
notifications (state, { type, count }) {
|
notifications (state, { type, count }: { type: NotificationsKey, count: number }) {
|
||||||
state.notifications[type] = count
|
state.notifications[type] = count
|
||||||
},
|
},
|
||||||
incrementNotifications (state, { type, count, value }) {
|
incrementNotifications (state, { type, count, value }: { type: NotificationsKey, count: number, value: number }) {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
state.notifications[type] = Math.max(0, value)
|
state.notifications[type] = Math.max(0, value)
|
||||||
} else {
|
} else {
|
||||||
|
@ -292,13 +351,13 @@ export default {
|
||||||
pageTitle: (state, value) => {
|
pageTitle: (state, value) => {
|
||||||
state.pageTitle = value
|
state.pageTitle = value
|
||||||
},
|
},
|
||||||
paginateBy: (state, { route, value }) => {
|
paginateBy: (state, { route, value }: { route: RouteWithPreferences, value: number }) => {
|
||||||
state.routePreferences[route].paginateBy = value
|
state.routePreferences[route].paginateBy = value
|
||||||
},
|
},
|
||||||
ordering: (state, { route, value }) => {
|
ordering: (state, { route, value }: { route: RouteWithPreferences, value: Ordering }) => {
|
||||||
state.routePreferences[route].ordering = value
|
state.routePreferences[route].ordering = value
|
||||||
},
|
},
|
||||||
orderingDirection: (state, { route, value }) => {
|
orderingDirection: (state, { route, value }: { route: RouteWithPreferences, value: OrderingDirection }) => {
|
||||||
state.routePreferences[route].orderingDirection = value
|
state.routePreferences[route].orderingDirection = value
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -307,40 +366,41 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
fetchUnreadNotifications ({ commit }, payload) {
|
fetchUnreadNotifications ({ commit }) {
|
||||||
axios.get('federation/inbox/', { params: { is_read: false, page_size: 1 } }).then((response) => {
|
axios.get('federation/inbox/', { params: { is_read: false, page_size: 1 } }).then((response) => {
|
||||||
commit('notifications', { type: 'inbox', count: response.data.count })
|
commit('notifications', { type: 'inbox', count: response.data.count })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchPendingReviewEdits ({ commit, rootState }, payload) {
|
fetchPendingReviewEdits ({ commit }) {
|
||||||
axios.get('mutations/', { params: { is_approved: 'null', page_size: 1 } }).then((response) => {
|
axios.get('mutations/', { params: { is_approved: 'null', page_size: 1 } }).then((response) => {
|
||||||
commit('notifications', { type: 'pendingReviewEdits', count: response.data.count })
|
commit('notifications', { type: 'pendingReviewEdits', count: response.data.count })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchPendingReviewReports ({ commit, rootState }, payload) {
|
fetchPendingReviewReports ({ commit }) {
|
||||||
axios.get('manage/moderation/reports/', { params: { is_handled: 'false', page_size: 1 } }).then((response) => {
|
axios.get('manage/moderation/reports/', { params: { is_handled: 'false', page_size: 1 } }).then((response) => {
|
||||||
commit('notifications', { type: 'pendingReviewReports', count: response.data.count })
|
commit('notifications', { type: 'pendingReviewReports', count: response.data.count })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchPendingReviewRequests ({ commit, rootState }, payload) {
|
fetchPendingReviewRequests ({ commit }) {
|
||||||
axios.get('manage/moderation/requests/', { params: { status: 'pending', page_size: 1 } }).then((response) => {
|
axios.get('manage/moderation/requests/', { params: { status: 'pending', page_size: 1 } }).then((response) => {
|
||||||
commit('notifications', { type: 'pendingReviewRequests', count: response.data.count })
|
commit('notifications', { type: 'pendingReviewRequests', count: response.data.count })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async currentLanguage ({ state, commit, rootState }, value) {
|
async currentLanguage ({ commit, rootState }, value) {
|
||||||
commit('currentLanguage', value)
|
commit('currentLanguage', value)
|
||||||
if (rootState.auth.authenticated) {
|
if (rootState.auth.authenticated) {
|
||||||
await axios.post('users/settings', { language: value })
|
await axios.post('users/settings', { language: value })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
websocketEvent ({ state }, event) {
|
websocketEvent ({ state }, event: WebSocketEvent) {
|
||||||
const handlers = state.websocketEventsHandlers[event.type]
|
const handlers = state.websocketEventsHandlers[event.type]
|
||||||
console.log('Dispatching websocket event', event, handlers)
|
console.log('Dispatching websocket event', event, handlers)
|
||||||
if (!handlers) {
|
if (!handlers) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const names = Object.keys(handlers)
|
const names = Object.keys(handlers)
|
||||||
names.forEach((k) => {
|
names.forEach((k) => {
|
||||||
const handler = handlers[k]
|
const handler = handlers[k]
|
||||||
|
@ -349,3 +409,5 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default store
|
||||||
|
|
|
@ -1,44 +1,45 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import useLogger from '~/composables/useLogger'
|
import useLogger from '~/composables/useLogger'
|
||||||
|
import { ListenWSEvent } from '~/types'
|
||||||
|
import { RootState } from '~/store'
|
||||||
|
import { Store } from 'vuex'
|
||||||
|
import { CurrentRadio, PopulateQueuePayload } from '~/store/radios'
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
// import axios from 'axios'
|
export const CLIENT_RADIOS = {
|
||||||
|
|
||||||
const RADIOS = {
|
|
||||||
// some radios are client side only, so we have to implement the populateQueue
|
// some radios are client side only, so we have to implement the populateQueue
|
||||||
// method by hand
|
// method by hand
|
||||||
account: {
|
account: {
|
||||||
offset: 1,
|
offset: 1,
|
||||||
populateQueue ({ current, dispatch, playNow }) {
|
populateQueue ({ current, dispatch, playNow }: PopulateQueuePayload) {
|
||||||
const params = { scope: `actor:${current.objectId.fullUsername}`, ordering: '-creation_date', page_size: 1, page: this.offset }
|
const params = { scope: `actor:${current.objectId.fullUsername}`, ordering: '-creation_date', page_size: 1, page: this.offset }
|
||||||
axios.get('history/listenings', { params }).then((response) => {
|
axios.get('history/listenings', { params }).then(async (response) => {
|
||||||
const latest = response.data.results[0]
|
const latest = response.data.results[0]
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
logger.error('No more tracks')
|
logger.error('No more tracks')
|
||||||
dispatch('stop')
|
await dispatch('stop')
|
||||||
}
|
}
|
||||||
|
|
||||||
this.offset += 1
|
this.offset += 1
|
||||||
const append = dispatch('queue/append', { track: latest.track }, { root: true })
|
const append = dispatch('queue/append', { track: latest.track }, { root: true })
|
||||||
if (playNow) {
|
if (playNow) {
|
||||||
append.then(() => {
|
append.then(() => dispatch('queue/last', null, { root: true }))
|
||||||
dispatch('queue/last', null, { root: true })
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}, (error) => {
|
}, async (error) => {
|
||||||
logger.error('Error while fetching listenings', error)
|
logger.error('Error while fetching listenings', error)
|
||||||
dispatch('stop')
|
await dispatch('stop')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
stop () {
|
stop () {
|
||||||
this.offset = 1
|
this.offset = 1
|
||||||
},
|
},
|
||||||
handleListen (current, event, store) {
|
handleListen (current: CurrentRadio, event: ListenWSEvent, store: Store<RootState>) {
|
||||||
// XXX: handle actors from other pods
|
// TODO: handle actors from other pods
|
||||||
if (event.actor.local_id === current.objectId.username) {
|
if (event.actor.local_id === current.objectId.username) {
|
||||||
axios.get(`tracks/${event.object.local_id}`).then((response) => {
|
axios.get(`tracks/${event.object.local_id}`).then(async (response) => {
|
||||||
if (response.data.uploads.length > 0) {
|
if (response.data.uploads.length > 0) {
|
||||||
store.dispatch('queue/append', { track: response.data })
|
await store.dispatch('queue/append', { track: response.data })
|
||||||
this.offset += 1
|
this.offset += 1
|
||||||
}
|
}
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
|
@ -48,6 +49,3 @@ const RADIOS = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function getClientOnlyRadio ({ type }) {
|
|
||||||
return RADIOS[type]
|
|
||||||
}
|
|
|
@ -44,13 +44,6 @@ export function getCookie (name: string) {
|
||||||
?.split('=')[1]
|
?.split('=')[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCsrf (xhr: XMLHttpRequest) {
|
|
||||||
const token = getCookie('csrftoken')
|
|
||||||
if (token) {
|
|
||||||
xhr.setRequestHeader('X-CSRFToken', token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO (wvffle): Use navigation guards
|
// TODO (wvffle): Use navigation guards
|
||||||
export async function checkRedirectToLogin (store: Store<any>, router: Router) {
|
export async function checkRedirectToLogin (store: Store<any>, router: Router) {
|
||||||
if (!store.state.auth.authenticated) {
|
if (!store.state.auth.authenticated) {
|
||||||
|
|
|
@ -236,9 +236,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
|
||||||
events: state => state.instance.events
|
|
||||||
}),
|
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
additionalNotifications: 'ui/additionalNotifications',
|
additionalNotifications: 'ui/additionalNotifications',
|
||||||
showInstanceSupportMessage: 'ui/showInstanceSupportMessage',
|
showInstanceSupportMessage: 'ui/showInstanceSupportMessage',
|
||||||
|
@ -283,21 +280,18 @@ export default {
|
||||||
newDisplayDate = null
|
newDisplayDate = null
|
||||||
}
|
}
|
||||||
payload[field] = newDisplayDate
|
payload[field] = newDisplayDate
|
||||||
const self = this
|
|
||||||
axios.patch(`users/${this.$store.state.auth.username}/`, payload).then((response) => {
|
axios.patch(`users/${this.$store.state.auth.username}/`, payload).then((response) => {
|
||||||
self.$store.commit('auth/profilePartialUpdate', response.data)
|
this.$store.commit('auth/profilePartialUpdate', response.data)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetch (params) {
|
fetch (params) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
const self = this
|
|
||||||
axios.get('federation/inbox/', { params: params }).then(response => {
|
axios.get('federation/inbox/', { params: params }).then(response => {
|
||||||
self.isLoading = false
|
this.isLoading = false
|
||||||
self.notifications = response.data
|
this.notifications = response.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
markAllAsRead () {
|
markAllAsRead () {
|
||||||
const self = this
|
|
||||||
const before = this.notifications.results[0].id
|
const before = this.notifications.results[0].id
|
||||||
const payload = {
|
const payload = {
|
||||||
action: 'read',
|
action: 'read',
|
||||||
|
@ -308,8 +302,8 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
axios.post('federation/inbox/action/', payload).then(response => {
|
axios.post('federation/inbox/action/', payload).then(response => {
|
||||||
self.$store.commit('ui/notifications', { type: 'inbox', count: 0 })
|
this.$store.commit('ui/notifications', { type: 'inbox', count: 0 })
|
||||||
self.notifications.results.forEach(n => {
|
this.notifications.results.forEach(n => {
|
||||||
n.is_read = true
|
n.is_read = true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import LoginForm from '~/components/auth/LoginForm.vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useGettext } from 'vue3-gettext'
|
import { useGettext } from 'vue3-gettext'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
next?: string
|
next?: string
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<i
|
<i
|
||||||
role="button"
|
role="button"
|
||||||
class="close icon"
|
class="close icon"
|
||||||
@click="pendingUploads = []"
|
@click="pendingUploads.length = 0"
|
||||||
/>
|
/>
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
<translate translate-context="Content/Channel/Header">
|
<translate translate-context="Content/Channel/Header">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { humanSize } from '~/utils/filters'
|
import { humanSize } from '~/utils/filters'
|
||||||
import { useGettext } from 'vue3-gettext'
|
import { useGettext } from 'vue3-gettext'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useStore } from 'vuex'
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
const { $pgettext } = useGettext()
|
const { $pgettext } = useGettext()
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"lib": ["dom", "esnext", "webworker"],
|
"lib": ["dom", "esnext", "webworker"],
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
|
|
632
front/yarn.lock
632
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue