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-router": "4.0.14",
|
||||
"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-lazyload": "0.2.5-beta",
|
||||
"vuedraggable": "4.1.0",
|
||||
|
@ -55,27 +55,26 @@
|
|||
"@types/jquery": "3.5.14",
|
||||
"@types/lodash-es": "4.17.6",
|
||||
"@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",
|
||||
"@vue/compiler-sfc": "3.2.33",
|
||||
"@vue/eslint-config-standard": "6.1.0",
|
||||
"@vue/eslint-config-typescript": "10.0.0",
|
||||
"@vue/test-utils": "1.3.0",
|
||||
"autoprefixer": "10.4.4",
|
||||
"chai": "4.3.6",
|
||||
"easygettext": "2.17.0",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-config-standard": "16.0.3",
|
||||
"eslint": "8.14.0",
|
||||
"eslint-config-standard": "17.0.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-promise": "6.0.0",
|
||||
"eslint-plugin-vue": "7.20.0",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"jest-cli": "27.5.1",
|
||||
"moxios": "0.4.0",
|
||||
"sinon": "13.0.2",
|
||||
"ts-jest": "27.1.4",
|
||||
"typescript": "^4.6.3",
|
||||
"typescript": "4.6.3",
|
||||
"vite": "2.8.6",
|
||||
"vite-plugin-pwa": "0.12.0",
|
||||
"vue-jest": "3.0.7",
|
||||
|
|
|
@ -12,7 +12,6 @@ import ReportModal from '~/components/moderation/ReportModal.vue'
|
|||
import { useIntervalFn, useToggle, useWindowSize } from '@vueuse/core'
|
||||
|
||||
import { computed, nextTick, onMounted, ref, watchEffect } from 'vue'
|
||||
import store from '~/store'
|
||||
import {
|
||||
ListenWSEvent,
|
||||
PendingReviewEditsWSEvent,
|
||||
|
@ -21,8 +20,11 @@ import {
|
|||
Track
|
||||
} from '~/types'
|
||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||
import { getClientOnlyRadio } from '~/radios'
|
||||
import { CLIENT_RADIOS } from '~/utils/clientRadios'
|
||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
// Tracks
|
||||
const currentTrack = computed(() => store.getters['queue/currentTrack'])
|
||||
|
@ -49,7 +51,7 @@ watchEffect(() => {
|
|||
|
||||
// Styles
|
||||
const customStylesheets = computed(() => {
|
||||
return store.state.instance?.frontSettings?.additionalStylesheets ?? []
|
||||
return store.state.instance.frontSettings.additionalStylesheets ?? []
|
||||
})
|
||||
|
||||
// Fake content
|
||||
|
@ -93,10 +95,10 @@ useWebSocketHandler('user_request.created', (event) => {
|
|||
|
||||
useWebSocketHandler('Listen', (event) => {
|
||||
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') {
|
||||
getClientOnlyRadio(current).handleListen(current, event as ListenWSEvent, store)
|
||||
if (current?.clientOnly) {
|
||||
CLIENT_RADIOS[current.type].handleListen(current, event as ListenWSEvent, store)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -345,7 +345,8 @@
|
|||
</section>
|
||||
</template>
|
||||
<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 moment from 'moment'
|
||||
import { sum } from 'lodash-es'
|
||||
|
|
|
@ -140,7 +140,7 @@ export default {
|
|||
instances.push(serverUrl)
|
||||
}
|
||||
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 }))
|
||||
},
|
||||
instanceHostname () {
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
</template>
|
||||
<content-form
|
||||
v-if="setting.fieldType === 'markdown'"
|
||||
v-bind="setting.fieldParams"
|
||||
v-model="values[setting.identifier]"
|
||||
v-bind="setting.fieldParams"
|
||||
/>
|
||||
<signup-form-builder
|
||||
v-else-if="setting.fieldType === 'formBuilder'"
|
||||
|
@ -158,6 +158,7 @@
|
|||
import axios from 'axios'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import SignupFormBuilder from '~/components/admin/SignupFormBuilder.vue'
|
||||
import useFormData from '~/composables/useFormData'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -207,20 +208,27 @@ export default {
|
|||
let contentType = 'application/json'
|
||||
const fileSettingsIDs = this.fileSettings.map((s) => { return s.identifier })
|
||||
if (fileSettingsIDs.length > 0) {
|
||||
contentType = 'multipart/form-data'
|
||||
postData = new FormData()
|
||||
this.settings.forEach((s) => {
|
||||
if (fileSettingsIDs.indexOf(s.identifier) > -1) {
|
||||
const input = self.$refs[s.identifier][0]
|
||||
const files = input.files
|
||||
console.log('ref', input, files)
|
||||
if (files && files.length > 0) {
|
||||
postData.append(s.identifier, files[0])
|
||||
const data = this.settings.reduce((data, setting) => {
|
||||
if (fileSettingsIDs.includes(setting.identifier)) {
|
||||
const [input] = this.$refs[setting.identifier]
|
||||
const { files } = input
|
||||
|
||||
// TODO (wvffle): Move to the top of setup
|
||||
const logger = useLogger()
|
||||
logger.debug('ref', input, files)
|
||||
|
||||
if (files?.length > 0) {
|
||||
data[s.identifier] = files[0]
|
||||
}
|
||||
} 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, {
|
||||
headers: {
|
||||
|
|
|
@ -320,7 +320,8 @@
|
|||
</template>
|
||||
|
||||
<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 { Howl, Howler } from 'howler'
|
||||
import { throttle, reverse } from 'lodash-es'
|
||||
|
|
|
@ -146,6 +146,8 @@ import axios from 'axios'
|
|||
|
||||
import { checkRedirectToLogin } from '~/utils'
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
import useFormData from '~/composables/useFormData'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
clientId: { type: String, required: true },
|
||||
|
@ -252,14 +254,16 @@ export default {
|
|||
submit () {
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
const data = new FormData()
|
||||
data.set('redirect_uri', this.redirectUri)
|
||||
data.set('scope', this.scope)
|
||||
data.set('allow', true)
|
||||
data.set('client_id', this.clientId)
|
||||
data.set('response_type', this.responseType)
|
||||
data.set('state', this.state)
|
||||
data.set('nonce', this.nonce)
|
||||
const data = useFormData({
|
||||
redirect_uri: this.redirectUri,
|
||||
scope: this.scope,
|
||||
allow: true,
|
||||
client_id: this.clientId,
|
||||
response_type: this.responseType,
|
||||
state: this.state,
|
||||
nonce: this.nonce
|
||||
})
|
||||
|
||||
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') {
|
||||
self.isLoading = false
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template v-if="$store.getters['instance/appDomain'] === $store.getters['instance/domain']">
|
||||
<template v-if="document.domain === $store.getters['instance/domain']">
|
||||
<div class="field">
|
||||
<label for="username-field">
|
||||
<translate translate-context="Content/Login/Input.Label/Noun">Username or e-mail address</translate>
|
||||
|
@ -133,7 +133,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
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()
|
||||
} else {
|
||||
this.isLoading = true
|
||||
|
@ -141,8 +141,7 @@ export default {
|
|||
}
|
||||
},
|
||||
async submitSession () {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
this.isLoading = true
|
||||
this.error = ''
|
||||
const credentials = {
|
||||
username: this.credentials.username,
|
||||
|
@ -160,7 +159,7 @@ export default {
|
|||
}
|
||||
}
|
||||
})
|
||||
.then(e => {
|
||||
.then(() => {
|
||||
self.isLoading = false
|
||||
})
|
||||
}
|
||||
|
|
|
@ -205,16 +205,15 @@
|
|||
</div>
|
||||
<file-upload-widget
|
||||
ref="upload"
|
||||
v-model="filesModel"
|
||||
:class="['ui', 'icon', 'basic', 'button', 'channels', {hidden: step === 3}]"
|
||||
:post-action="uploadUrl"
|
||||
:multiple="true"
|
||||
:data="baseImportMetadata"
|
||||
:drop="true"
|
||||
:extensions="$store.state.ui.supportedExtensions"
|
||||
:value="files"
|
||||
name="audio_file"
|
||||
:thread="1"
|
||||
@input="updateFiles"
|
||||
@input-file="inputFile"
|
||||
>
|
||||
<div>
|
||||
|
@ -292,6 +291,15 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
filesModel: {
|
||||
get () {
|
||||
return this.files
|
||||
},
|
||||
|
||||
set (value) {
|
||||
this.updateFiles(value)
|
||||
}
|
||||
},
|
||||
labels () {
|
||||
return {
|
||||
editTitle: this.$pgettext('Content/*/Button.Label/Verb', 'Edit')
|
||||
|
@ -419,9 +427,7 @@ export default {
|
|||
return uploaded
|
||||
},
|
||||
activeFile () {
|
||||
return this.files.filter((f) => {
|
||||
return f.active
|
||||
})[0]
|
||||
return this.files.find((file) => file.active)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -585,15 +591,14 @@ export default {
|
|||
this[objName] = Object.assign({}, this[objName], newData)
|
||||
},
|
||||
updateFiles (value) {
|
||||
const self = this
|
||||
this.files = value
|
||||
this.files.forEach((f) => {
|
||||
if (f.response && f.response.uuid && self.audioMetadata[f.response.uuid] === undefined) {
|
||||
self.uploadData[f.response.uuid] = f.response
|
||||
self.setDynamic('uploadImportData', f.response.uuid, {
|
||||
if (f.response?.uuid && this.audioMetadata[f.response.uuid] === undefined) {
|
||||
this.uploadData[f.response.uuid] = f.response
|
||||
this.setDynamic('uploadImportData', f.response.uuid, {
|
||||
...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 { reactive, ref, watch } from 'vue'
|
||||
import { BackendError } from '~/types'
|
||||
import { useStore } from 'vuex'
|
||||
import { useStore } from '~/store'
|
||||
import useFormData from '~/composables/useFormData'
|
||||
|
||||
interface Props {
|
||||
modelValue: string | null
|
||||
|
@ -35,8 +36,7 @@ const submit = async () => {
|
|||
errors.length = 0
|
||||
file.value = input.value.files[0]
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.value)
|
||||
const formData = useFormData({ file: file.value })
|
||||
|
||||
try {
|
||||
const { data } = await axios.post('attachments/', formData, {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
import { useToggle } from '@vueuse/core'
|
||||
|
||||
interface Props {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import $ from 'jquery'
|
||||
import { onMounted } from 'vue'
|
||||
import store from '~/store'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
interface Message {
|
||||
content: string
|
||||
|
@ -10,6 +10,7 @@ interface Message {
|
|||
|
||||
const props = defineProps<{ message: Message }>()
|
||||
|
||||
const store = useStore()
|
||||
onMounted(() => {
|
||||
const params = {
|
||||
context: '#app',
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { computed, ref } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useClipboard, useVModel } from '@vueuse/core'
|
||||
import { useStore } from 'vuex'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
|
|
|
@ -1,44 +1,72 @@
|
|||
<script>
|
||||
import FileUpload from 'vue-upload-component'
|
||||
import { setCsrf } from '~/utils'
|
||||
<script setup lang="ts">
|
||||
import FileUpload, { VueUploadItem } from 'vue-upload-component'
|
||||
import { getCookie } from '~/utils'
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
export default {
|
||||
extends: FileUpload,
|
||||
methods: {
|
||||
uploadHtml5 (file) {
|
||||
const form = new window.FormData()
|
||||
const filename = file.file.filename || file.name
|
||||
let value
|
||||
const data = { ...file.data }
|
||||
if (data.import_metadata) {
|
||||
data.import_metadata = { ...(data.import_metadata || {}) }
|
||||
if (data.channel && !data.import_metadata.title) {
|
||||
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 instance = getCurrentInstance()
|
||||
const attrs = instance?.attrs ?? {}
|
||||
|
||||
const store = useStore()
|
||||
const headers = computed(() => {
|
||||
const headers: Record<string, string> = typeof attrs.headers === 'object'
|
||||
? { ...attrs.headers }
|
||||
: {}
|
||||
|
||||
if (store.state.auth.oauth.accessToken) {
|
||||
headers.Authorization ??= store.getters['auth/header']
|
||||
}
|
||||
|
||||
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 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>
|
||||
import axios from 'axios'
|
||||
import { mapState, useStore } from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Modal from '~/components/semantic/Modal.vue'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
|
|
|
@ -155,10 +155,11 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import { mapState, useStore } from 'vuex'
|
||||
import { mapState } from 'vuex'
|
||||
import { computed } from 'vue'
|
||||
import ReportCategoryDropdown from '~/components/moderation/ReportCategoryDropdown.vue'
|
||||
import Modal from '~/components/semantic/Modal.vue'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
function urlDomain (data) {
|
||||
const a = document.createElement('a')
|
||||
|
|
|
@ -3,7 +3,7 @@ import $ from 'jquery'
|
|||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||
import { computed, onBeforeUnmount, ref, watchEffect } from 'vue'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { useStore } from 'vuex'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
interface Props {
|
||||
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 { tryOnScopeDispose } from '@vueuse/core'
|
||||
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()}`
|
||||
store.commit('ui/addWebsocketEventHandler', { eventName, handler, id })
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ export const install: InitModule = async ({ store, router }) => {
|
|||
// 3. use the current url
|
||||
const defaultInstanceUrl = store.state.instance.frontSettings.defaultServerUrl ||
|
||||
import.meta.env.VUE_APP_INSTANCE_URL ||
|
||||
store.getters['instance/defaultUrl']()
|
||||
location.origin
|
||||
|
||||
store.commit('instance/instanceUrl', defaultInstanceUrl)
|
||||
} else {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { InitModule } from '~/types'
|
|||
import store from '~/store'
|
||||
|
||||
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
|
||||
return map
|
||||
}, {})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import router from '~/router'
|
||||
import store from '~/store'
|
||||
import store, { key } from '~/store'
|
||||
// @ts-expect-error typescript does not know about configureCompat
|
||||
import { configureCompat, createApp, defineAsyncComponent, h } from 'vue'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
@ -33,7 +33,7 @@ const app = createApp({
|
|||
})
|
||||
|
||||
app.use(router)
|
||||
app.use(store)
|
||||
app.use(store, key)
|
||||
|
||||
const modules: Array<Promise<unknown>> = []
|
||||
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): 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): 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): Use navigation guards
|
||||
|
|
|
@ -618,7 +618,7 @@ export default createRouter({
|
|||
component: () =>
|
||||
import('~/components/library/Home.vue'),
|
||||
name: 'library.me',
|
||||
props: route => ({
|
||||
props: () => ({
|
||||
scope: 'me'
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,27 +1,51 @@
|
|||
import axios from 'axios'
|
||||
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()
|
||||
|
||||
function getDefaultScopedTokens () {
|
||||
function getDefaultScopedTokens (): ScopedTokens {
|
||||
return {
|
||||
listen: null
|
||||
}
|
||||
}
|
||||
|
||||
function asForm (obj) {
|
||||
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 () {
|
||||
function getDefaultOauth (): OAuthTokens {
|
||||
return {
|
||||
clientId: null,
|
||||
clientSecret: null,
|
||||
|
@ -29,20 +53,18 @@ function getDefaultOauth () {
|
|||
refreshToken: null
|
||||
}
|
||||
}
|
||||
const NEEDED_SCOPES = [
|
||||
'read',
|
||||
'write'
|
||||
].join(' ')
|
||||
async function createOauthApp (domain) {
|
||||
|
||||
async function createOauthApp () {
|
||||
const payload = {
|
||||
name: `Funkwhale web client at ${window.location.hostname}`,
|
||||
website: baseUrl,
|
||||
website: location.origin,
|
||||
scopes: NEEDED_SCOPES,
|
||||
redirect_uris: `${baseUrl}/auth/callback`
|
||||
redirect_uris: `${location.origin}/auth/callback`
|
||||
}
|
||||
return (await axios.post('oauth/apps/', payload)).data
|
||||
}
|
||||
export default {
|
||||
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
authenticated: false,
|
||||
|
@ -73,10 +95,9 @@ export default {
|
|||
state.scopedTokens = getDefaultScopedTokens()
|
||||
state.oauth = getDefaultOauth()
|
||||
state.availablePermissions = {
|
||||
federation: false,
|
||||
settings: false,
|
||||
library: false,
|
||||
upload: false
|
||||
moderation: false
|
||||
}
|
||||
},
|
||||
profile: (state, value) => {
|
||||
|
@ -85,11 +106,15 @@ export default {
|
|||
authenticated: (state, value) => {
|
||||
state.authenticated = value
|
||||
if (value === false) {
|
||||
state.username = null
|
||||
state.fullUsername = null
|
||||
state.username = ''
|
||||
state.fullUsername = ''
|
||||
state.profile = null
|
||||
state.scopedTokens = getDefaultScopedTokens()
|
||||
state.availablePermissions = {}
|
||||
state.availablePermissions = {
|
||||
settings: false,
|
||||
library: false,
|
||||
moderation: false
|
||||
}
|
||||
}
|
||||
},
|
||||
username: (state, value) => {
|
||||
|
@ -106,13 +131,17 @@ export default {
|
|||
scopedTokens: (state, value) => {
|
||||
state.scopedTokens = { ...value }
|
||||
},
|
||||
permission: (state, { key, status }) => {
|
||||
permission: (state, { key, status }: { key: Permission, status: boolean }) => {
|
||||
state.availablePermissions[key] = status
|
||||
},
|
||||
profilePartialUpdate: (state, payload) => {
|
||||
Object.keys(payload).forEach((k) => {
|
||||
state.profile[k] = payload[k]
|
||||
})
|
||||
profilePartialUpdate: (state, payload: Profile) => {
|
||||
if (!state.profile) {
|
||||
state.profile = {} as Profile
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
state.profile[key as keyof Profile] = value
|
||||
}
|
||||
},
|
||||
oauthApp: (state, payload) => {
|
||||
state.oauth.clientId = payload.client_id
|
||||
|
@ -125,12 +154,9 @@ export default {
|
|||
},
|
||||
actions: {
|
||||
// Send a request to the login URL and save the returned JWT
|
||||
login ({ commit, dispatch }, { next, credentials, onError }) {
|
||||
const form = new FormData()
|
||||
Object.keys(credentials).forEach((k) => {
|
||||
form.set(k, credentials[k])
|
||||
})
|
||||
return axios.post('users/login', form).then(response => {
|
||||
login ({ dispatch }, { next, credentials, onError }) {
|
||||
const form = useFormData(credentials)
|
||||
return axios.post('users/login', form).then(() => {
|
||||
logger.info('Successfully logged in as', credentials.username)
|
||||
dispatch('fetchProfile').then(() => {
|
||||
// Redirect to a specified route
|
||||
|
@ -143,7 +169,7 @@ export default {
|
|||
onError(response)
|
||||
})
|
||||
},
|
||||
async logout ({ state, commit }) {
|
||||
async logout ({ commit }) {
|
||||
try {
|
||||
await axios.post('users/logout')
|
||||
} catch (error) {
|
||||
|
@ -162,7 +188,7 @@ export default {
|
|||
})
|
||||
logger.info('Log out, goodbye!')
|
||||
},
|
||||
fetchProfile ({ commit, dispatch, state }) {
|
||||
fetchProfile ({ dispatch }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get('users/me/').then((response) => {
|
||||
logger.info('Successfully fetched user profile')
|
||||
|
@ -181,39 +207,34 @@ export default {
|
|||
dispatch('moderation/fetchContentFilters', null, { root: true })
|
||||
dispatch('playlists/fetchOwn', null, { root: true })
|
||||
resolve(response.data)
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.info('Error while fetching user profile')
|
||||
reject(new Error('Error while fetching user profile'))
|
||||
})
|
||||
})
|
||||
},
|
||||
updateProfile ({ commit }, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
commit('authenticated', true)
|
||||
commit('profile', data)
|
||||
commit('username', data.username)
|
||||
commit('fullUsername', data.full_username)
|
||||
if (data.tokens) {
|
||||
commit('scopedTokens', data.tokens)
|
||||
}
|
||||
Object.keys(data.permissions).forEach(function (key) {
|
||||
// this makes it easier to check for permissions in templates
|
||||
commit('permission', {
|
||||
key,
|
||||
status: data.permissions[String(key)]
|
||||
})
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
commit('authenticated', true)
|
||||
commit('profile', data)
|
||||
commit('username', data.username)
|
||||
commit('fullUsername', data.full_username)
|
||||
if (data.tokens) {
|
||||
commit('scopedTokens', data.tokens)
|
||||
}
|
||||
|
||||
for (const [permission, hasPermission] of Object.entries(data.permissions)) {
|
||||
// this makes it easier to check for permissions in templates
|
||||
commit('permission', { key: permission, status: hasPermission })
|
||||
}
|
||||
},
|
||||
async oauthLogin ({ state, rootState, commit, getters }, next) {
|
||||
const app = await createOauthApp(getters.appDomain)
|
||||
async oauthLogin ({ state, rootState, commit }, next) {
|
||||
const app = await createOauthApp()
|
||||
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 authorizeUrl = `${rootState.instance.instanceUrl}authorize?${params}`
|
||||
console.log('Redirecting user...', authorizeUrl)
|
||||
window.location = authorizeUrl
|
||||
window.location.href = authorizeUrl
|
||||
},
|
||||
async handleOauthCallback ({ state, commit, dispatch }, authorizationCode) {
|
||||
console.log('Fetching token...')
|
||||
|
@ -222,17 +243,17 @@ export default {
|
|||
client_secret: state.oauth.clientSecret,
|
||||
grant_type: 'authorization_code',
|
||||
code: authorizationCode,
|
||||
redirect_uri: `${baseUrl}/auth/callback`
|
||||
redirect_uri: `${location.origin}/auth/callback`
|
||||
}
|
||||
const response = await axios.post(
|
||||
'oauth/token/',
|
||||
asForm(payload),
|
||||
useFormData(payload),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
)
|
||||
commit('oauthToken', response.data)
|
||||
await dispatch('fetchProfile')
|
||||
},
|
||||
async refreshOauthToken ({ state, commit }, authorizationCode) {
|
||||
async refreshOauthToken ({ state, commit }) {
|
||||
const payload = {
|
||||
client_id: state.oauth.clientId,
|
||||
client_secret: state.oauth.clientSecret,
|
||||
|
@ -241,10 +262,12 @@ export default {
|
|||
}
|
||||
const response = await axios.post(
|
||||
'oauth/token/',
|
||||
asForm(payload),
|
||||
useFormData(payload),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
)
|
||||
commit('oauthToken', response.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,9 +1,36 @@
|
|||
import axios from 'axios'
|
||||
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()
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
subscriptions: [],
|
||||
|
@ -17,19 +44,20 @@ export default {
|
|||
mutations: {
|
||||
subscriptions: (state, { uuid, value }) => {
|
||||
if (value) {
|
||||
if (state.subscriptions.indexOf(uuid) === -1) {
|
||||
if (!state.subscriptions.includes(uuid)) {
|
||||
state.subscriptions.push(uuid)
|
||||
}
|
||||
} else {
|
||||
const i = state.subscriptions.indexOf(uuid)
|
||||
if (i > -1) {
|
||||
state.subscriptions.splice(i, 1)
|
||||
const index = state.subscriptions.indexOf(uuid)
|
||||
if (index > -1) {
|
||||
state.subscriptions.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
state.count = state.subscriptions.length
|
||||
},
|
||||
reset (state) {
|
||||
state.subscriptions = []
|
||||
state.subscriptions.length = 0
|
||||
state.count = 0
|
||||
},
|
||||
showUploadModal (state, value) {
|
||||
|
@ -50,24 +78,22 @@ export default {
|
|||
}
|
||||
},
|
||||
getters: {
|
||||
isSubscribed: (state) => (uuid) => {
|
||||
return state.subscriptions.indexOf(uuid) > -1
|
||||
}
|
||||
isSubscribed: (state) => (uuid: string) => state.subscriptions.includes(uuid)
|
||||
},
|
||||
actions: {
|
||||
set ({ commit, state }, { uuid, value }) {
|
||||
set ({ commit }, { uuid, value }) {
|
||||
commit('subscriptions', { uuid, value })
|
||||
if (value) {
|
||||
return axios.post(`channels/${uuid}/subscribe/`).then((response) => {
|
||||
return axios.post(`channels/${uuid}/subscribe/`).then(() => {
|
||||
logger.info('Successfully subscribed to channel')
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.info('Error while subscribing to channel')
|
||||
commit('subscriptions', { uuid, value: !value })
|
||||
})
|
||||
} else {
|
||||
return axios.post(`channels/${uuid}/unsubscribe/`).then((response) => {
|
||||
return axios.post(`channels/${uuid}/unsubscribe/`).then(() => {
|
||||
logger.info('Successfully unsubscribed from channel')
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.info('Error while unsubscribing from channel')
|
||||
commit('subscriptions', { uuid, value: !value })
|
||||
})
|
||||
|
@ -76,13 +102,15 @@ export default {
|
|||
toggle ({ getters, dispatch }, uuid) {
|
||||
dispatch('set', { uuid, value: !getters.isSubscribed(uuid) })
|
||||
},
|
||||
fetchSubscriptions ({ dispatch, state, commit, rootState }, url) {
|
||||
fetchSubscriptions ({ commit }) {
|
||||
const promise = axios.get('subscriptions/all/')
|
||||
return promise.then((response) => {
|
||||
response.data.results.forEach(result => {
|
||||
response.data.results.forEach((result: { channel: unknown }) => {
|
||||
commit('subscriptions', { uuid: result.channel, value: true })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import axios from 'axios'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import { Module } from 'vuex'
|
||||
import { RootState } from '~/store/index'
|
||||
|
||||
export interface State {
|
||||
tracks: string[]
|
||||
count: number
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
tracks: [],
|
||||
|
@ -12,41 +19,42 @@ export default {
|
|||
mutations: {
|
||||
track: (state, { id, value }) => {
|
||||
if (value) {
|
||||
if (state.tracks.indexOf(id) === -1) {
|
||||
if (!state.tracks.includes(id)) {
|
||||
state.tracks.push(id)
|
||||
}
|
||||
} else {
|
||||
const i = state.tracks.indexOf(id)
|
||||
if (i > -1) {
|
||||
state.tracks.splice(i, 1)
|
||||
const index = state.tracks.indexOf(id)
|
||||
if (index > -1) {
|
||||
state.tracks.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
state.count = state.tracks.length
|
||||
},
|
||||
reset (state) {
|
||||
state.tracks = []
|
||||
state.tracks.length = 0
|
||||
state.count = 0
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isFavorite: (state) => (id) => {
|
||||
return state.tracks.indexOf(id) > -1
|
||||
isFavorite: (state) => (id: string) => {
|
||||
return state.tracks.includes(id)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
set ({ commit, state }, { id, value }) {
|
||||
set ({ commit }, { id, value }) {
|
||||
commit('track', { id, 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')
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.info('Error while adding track to favorites')
|
||||
commit('track', { id, value: !value })
|
||||
})
|
||||
} 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')
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.info('Error while removing track from favorites')
|
||||
commit('track', { id, value: !value })
|
||||
})
|
||||
|
@ -55,20 +63,22 @@ export default {
|
|||
toggle ({ getters, dispatch }, 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
|
||||
const params = {
|
||||
user: rootState.auth.profile.id,
|
||||
user: rootState.auth.profile?.id,
|
||||
page_size: 50,
|
||||
ordering: '-creation_date'
|
||||
}
|
||||
const promise = axios.get('favorites/tracks/all/', { params: params })
|
||||
return promise.then((response) => {
|
||||
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 })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 favorites from './favorites'
|
||||
import channels from './channels'
|
||||
import libraries from './libraries'
|
||||
import auth from './auth'
|
||||
import instance from './instance'
|
||||
import moderation from './moderation'
|
||||
import queue from './queue'
|
||||
import radios from './radios'
|
||||
import player from './player'
|
||||
import playlists from './playlists'
|
||||
import ui from './ui'
|
||||
import favorites, { State as FavoritesState } from './favorites'
|
||||
import channels, { State as ChannelsState } from './channels'
|
||||
import libraries, { State as LibrariesState } from './libraries'
|
||||
import auth, { State as AuthState } from './auth'
|
||||
import instance, { State as InstanceState } from './instance'
|
||||
import moderation, { State as ModerationState } from './moderation'
|
||||
import queue, { State as QueueState } from './queue'
|
||||
import radios, { State as RadiosState } from './radios'
|
||||
import player, { State as PlayerState } from './player'
|
||||
import playlists, { State as PlaylistsState } from './playlists'
|
||||
import ui, { State as UiState } from './ui'
|
||||
import { InjectionKey } from 'vue'
|
||||
|
||||
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')
|
||||
|
@ -43,7 +53,7 @@ export default createStore<RootState>({
|
|||
}),
|
||||
createPersistedState({
|
||||
key: 'instance',
|
||||
paths: ['instance.events', 'instance.instanceUrl', 'instance.knownInstances']
|
||||
paths: ['instance.instanceUrl', 'instance.knownInstances']
|
||||
}),
|
||||
createPersistedState({
|
||||
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 { merge } from 'lodash-es'
|
||||
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()
|
||||
|
||||
function getDefaultUrl () {
|
||||
return (
|
||||
window.location.protocol + '//' + window.location.hostname +
|
||||
(window.location.port ? ':' + window.location.port : '') + '/'
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
maxEvents: 200,
|
||||
frontSettings: {},
|
||||
instanceUrl: import.meta.env.VUE_APP_INSTANCE_URL,
|
||||
events: [],
|
||||
frontSettings: {
|
||||
defaultServerUrl: location.origin,
|
||||
additionalStylesheets: []
|
||||
},
|
||||
instanceUrl: import.meta.env.VUE_APP_INSTANCE_URL as string,
|
||||
knownInstances: [],
|
||||
nodeinfo: null,
|
||||
settings: {
|
||||
|
@ -63,15 +99,6 @@ export default {
|
|||
settings: (state, 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) => {
|
||||
state.nodeinfo = value
|
||||
},
|
||||
|
@ -82,6 +109,7 @@ export default {
|
|||
if (value && !value.endsWith('/')) {
|
||||
value = value + '/'
|
||||
}
|
||||
|
||||
state.instanceUrl = value
|
||||
|
||||
// append the URL to the list (and remove existing one if needed)
|
||||
|
@ -92,8 +120,9 @@ export default {
|
|||
}
|
||||
state.knownInstances.splice(0, 0, value)
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
axios.defaults.baseURL = null
|
||||
axios.defaults.baseURL = undefined
|
||||
return
|
||||
}
|
||||
const suffix = 'api/v1/'
|
||||
|
@ -101,18 +130,13 @@ export default {
|
|||
}
|
||||
},
|
||||
getters: {
|
||||
defaultUrl: (state) => () => {
|
||||
return getDefaultUrl()
|
||||
},
|
||||
absoluteUrl: (state) => (relativeUrl) => {
|
||||
if (relativeUrl.startsWith('http')) {
|
||||
return relativeUrl
|
||||
}
|
||||
if (state.instanceUrl?.endsWith('/') && relativeUrl.startsWith('/')) {
|
||||
absoluteUrl: (state) => (relativeUrl: string) => {
|
||||
if (relativeUrl.startsWith('http')) return relativeUrl
|
||||
if (state.instanceUrl.endsWith('/') && relativeUrl.startsWith('/')) {
|
||||
relativeUrl = relativeUrl.slice(1)
|
||||
}
|
||||
|
||||
const instanceUrl = state.instanceUrl ?? getDefaultUrl()
|
||||
const instanceUrl = state.instanceUrl ?? location.origin
|
||||
return instanceUrl + relativeUrl
|
||||
},
|
||||
domain: (state) => {
|
||||
|
@ -121,12 +145,10 @@ export default {
|
|||
parser.href = url
|
||||
return parser.hostname
|
||||
},
|
||||
appDomain: (state) => {
|
||||
return location.hostname
|
||||
}
|
||||
appDomain: () => location.hostname
|
||||
},
|
||||
actions: {
|
||||
setUrl ({ commit, dispatch }, url) {
|
||||
setUrl ({ commit }, url) {
|
||||
commit('instanceUrl', url)
|
||||
const modules = [
|
||||
'auth',
|
||||
|
@ -146,7 +168,8 @@ export default {
|
|||
return axios.get('instance/settings/').then(response => {
|
||||
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][entry.name] = entry
|
||||
return map
|
||||
|
@ -161,9 +184,11 @@ export default {
|
|||
fetchFrontSettings ({ commit }) {
|
||||
return axios.get('/settings.json').then(response => {
|
||||
commit('frontSettings', response.data)
|
||||
}, response => {
|
||||
}, () => {
|
||||
logger.error('Error when fetching front-end configuration (or no customization available)')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,41 +1,44 @@
|
|||
import axios from 'axios'
|
||||
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()
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
followedLibraries: [],
|
||||
followsByLibrary: {},
|
||||
count: 0
|
||||
},
|
||||
mutations: {
|
||||
follows: (state, { library, follow }) => {
|
||||
const replacement = { ...state.followsByLibrary }
|
||||
if (follow) {
|
||||
if (state.followedLibraries.indexOf(library) === -1) {
|
||||
state.followedLibraries.push(library)
|
||||
replacement[library] = follow
|
||||
}
|
||||
state.followsByLibrary[library] = follow
|
||||
} else {
|
||||
const i = state.followedLibraries.indexOf(library)
|
||||
if (i > -1) {
|
||||
state.followedLibraries.splice(i, 1)
|
||||
replacement[library] = null
|
||||
}
|
||||
delete state.followsByLibrary[library]
|
||||
}
|
||||
state.followsByLibrary = replacement
|
||||
state.count = state.followedLibraries.length
|
||||
|
||||
state.count = Object.keys(state.followsByLibrary).length
|
||||
},
|
||||
reset (state) {
|
||||
state.followedLibraries = []
|
||||
state.followsByLibrary = {}
|
||||
state.count = 0
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
follow: (state) => (library) => {
|
||||
follow: (state) => (library: string) => {
|
||||
return state.followsByLibrary[library]
|
||||
}
|
||||
},
|
||||
|
@ -45,16 +48,16 @@ export default {
|
|||
return axios.post('federation/follows/library/', { target: uuid }).then((response) => {
|
||||
logger.info('Successfully subscribed to library')
|
||||
commit('follows', { library: uuid, follow: response.data })
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.info('Error while subscribing to library')
|
||||
commit('follows', { library: uuid, follow: null })
|
||||
})
|
||||
} else {
|
||||
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')
|
||||
commit('follows', { library: uuid, follow: null })
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.info('Error while unsubscribing from library')
|
||||
commit('follows', { library: uuid, follow: follow })
|
||||
})
|
||||
|
@ -66,10 +69,12 @@ export default {
|
|||
fetchFollows ({ dispatch, state, commit, rootState }, url) {
|
||||
const promise = axios.get('federation/follows/library/all/')
|
||||
return promise.then((response) => {
|
||||
response.data.results.forEach(result => {
|
||||
response.data.results.forEach((result: { library: string }) => {
|
||||
commit('follows', { library: result.library, follow: result })
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,10 +1,35 @@
|
|||
import axios from 'axios'
|
||||
import { sortBy } from 'lodash-es'
|
||||
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()
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
filters: [],
|
||||
|
@ -56,10 +81,16 @@ export default {
|
|||
},
|
||||
reset (state) {
|
||||
state.filters = []
|
||||
state.filterModalTarget = null
|
||||
state.filterModalTarget = {
|
||||
type: null,
|
||||
target: null
|
||||
}
|
||||
state.showFilterModal = false
|
||||
state.showReportModal = false
|
||||
state.reportModalTarget = {}
|
||||
state.reportModalTarget = {
|
||||
type: null,
|
||||
target: null
|
||||
}
|
||||
},
|
||||
deleteContentFilter (state, uuid) {
|
||||
state.filters = state.filters.filter((e) => {
|
||||
|
@ -86,7 +117,7 @@ export default {
|
|||
commit('reportModalTarget', payload)
|
||||
commit('showReportModal', true)
|
||||
},
|
||||
fetchContentFilters ({ dispatch, state, commit, rootState }, url) {
|
||||
fetchContentFilters ({ dispatch, commit }, url) {
|
||||
let params = {}
|
||||
let promise
|
||||
if (url) {
|
||||
|
@ -104,15 +135,17 @@ export default {
|
|||
if (response.data.next) {
|
||||
dispatch('fetchContentFilters', response.data.next)
|
||||
}
|
||||
response.data.results.forEach(result => {
|
||||
response.data.results.forEach((result: ContentFilter) => {
|
||||
commit('contentFilter', result)
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteContentFilter ({ commit }, uuid) {
|
||||
return axios.delete(`moderation/content-filters/${uuid}/`).then((response) => {
|
||||
return axios.delete(`moderation/content-filters/${uuid}/`).then(() => {
|
||||
commit('deleteContentFilter', uuid)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
import axios from 'axios'
|
||||
import time from '~/utils/time'
|
||||
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()
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
maxConsecutiveErrors: 5,
|
||||
|
@ -79,12 +95,11 @@ export default {
|
|||
},
|
||||
getters: {
|
||||
durationFormatted: state => {
|
||||
let duration = parseInt(state.duration)
|
||||
if (duration % 1 !== 0) {
|
||||
if (state.duration % 1 !== 0) {
|
||||
return time.parse(0)
|
||||
}
|
||||
duration = Math.round(state.duration)
|
||||
return time.parse(duration)
|
||||
|
||||
return time.parse(Math.round(state.duration))
|
||||
},
|
||||
currentTimeFormatted: state => {
|
||||
return time.parse(Math.round(state.currentTime))
|
||||
|
@ -132,15 +147,15 @@ export default {
|
|||
commit('volume', state.tempVolume)
|
||||
}
|
||||
},
|
||||
trackListened ({ commit, rootState }, track) {
|
||||
trackListened ({ rootState }, track) {
|
||||
if (!rootState.auth.authenticated) {
|
||||
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')
|
||||
})
|
||||
},
|
||||
trackEnded ({ commit, dispatch, rootState }, track) {
|
||||
trackEnded ({ commit, dispatch, rootState }) {
|
||||
const queueState = rootState.queue
|
||||
if (queueState.currentIndex === queueState.tracks.length - 1) {
|
||||
// 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 { 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,
|
||||
state: {
|
||||
playlists: [],
|
||||
|
@ -26,18 +34,20 @@ export default {
|
|||
},
|
||||
actions: {
|
||||
async fetchOwn ({ commit, rootState }) {
|
||||
const userId = rootState.auth.profile.id
|
||||
if (!userId) {
|
||||
return
|
||||
}
|
||||
let playlists = []
|
||||
const userId = rootState.auth.profile?.id
|
||||
if (!userId) return
|
||||
|
||||
const playlists = []
|
||||
let url = 'playlists/'
|
||||
while (url != null) {
|
||||
const response = await axios.get(url, { params: { scope: 'me' } })
|
||||
playlists = [...playlists, ...response.data.results]
|
||||
playlists.push(...response.data.results)
|
||||
url = response.data.next
|
||||
}
|
||||
|
||||
commit('playlists', playlists)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import { shuffle } from 'lodash-es'
|
||||
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()
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
tracks: [],
|
||||
|
@ -12,7 +21,7 @@ export default {
|
|||
},
|
||||
mutations: {
|
||||
reset (state) {
|
||||
state.tracks = []
|
||||
state.tracks.length = 0
|
||||
state.currentIndex = -1
|
||||
state.ended = true
|
||||
},
|
||||
|
@ -62,7 +71,7 @@ export default {
|
|||
isEmpty: state => state.tracks.length === 0
|
||||
},
|
||||
actions: {
|
||||
append ({ commit, state, dispatch }, { track, index }) {
|
||||
append ({ commit, state }, { track, index }) {
|
||||
index = index || state.tracks.length
|
||||
if (index > state.tracks.length - 1) {
|
||||
// we simply push to the end
|
||||
|
@ -73,26 +82,28 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
appendMany ({ state, commit, dispatch }, { tracks, index, callback }) {
|
||||
logger.info('Appending many tracks to the queue', tracks.map(e => { return e.title }))
|
||||
appendMany ({ state, dispatch }, { tracks, index, callback }) {
|
||||
logger.info('Appending many tracks to the queue', tracks.map((track: Track) => track.title))
|
||||
let shouldPlay = false
|
||||
|
||||
if (state.tracks.length === 0) {
|
||||
index = 0
|
||||
shouldPlay = true
|
||||
} else {
|
||||
index = index || state.tracks.length
|
||||
index = index ?? state.tracks.length
|
||||
}
|
||||
|
||||
const total = tracks.length
|
||||
tracks.forEach((t, i) => {
|
||||
const p = dispatch('append', { track: t, index: index })
|
||||
tracks.forEach((track: Track, i: number) => {
|
||||
const promise = dispatch('append', { track: track, index: index })
|
||||
index += 1
|
||||
|
||||
if (callback && i + 1 === total) {
|
||||
p.then(callback)
|
||||
promise.then(callback)
|
||||
}
|
||||
if (shouldPlay && p && i + 1 === total) {
|
||||
p.then(() => {
|
||||
dispatch('next')
|
||||
})
|
||||
|
||||
if (shouldPlay && promise && i + 1 === total) {
|
||||
promise.then(() => dispatch('next'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -163,7 +174,7 @@ export default {
|
|||
async shuffle ({ dispatch, state }, callback) {
|
||||
const shuffled = shuffle(state.tracks)
|
||||
state.tracks.length = 0
|
||||
const params = { tracks: shuffled }
|
||||
const params: { tracks: Track[], callback?: () => unknown } = { tracks: shuffled }
|
||||
if (callback) {
|
||||
params.callback = callback
|
||||
}
|
||||
|
@ -172,3 +183,5 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,17 +1,40 @@
|
|||
import axios from 'axios'
|
||||
import { getClientOnlyRadio } from '~/radios'
|
||||
import { CLIENT_RADIOS } from '~/utils/clientRadios'
|
||||
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()
|
||||
|
||||
export default {
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
current: null,
|
||||
running: false
|
||||
},
|
||||
getters: {
|
||||
types: state => {
|
||||
types: () => {
|
||||
return {
|
||||
'actor-content': {
|
||||
name: 'Your content',
|
||||
|
@ -39,7 +62,7 @@ export default {
|
|||
mutations: {
|
||||
reset (state) {
|
||||
state.running = false
|
||||
state.current = false
|
||||
state.current = null
|
||||
},
|
||||
current: (state, value) => {
|
||||
state.current = value
|
||||
|
@ -67,13 +90,13 @@ export default {
|
|||
commit('current', { type, objectId, session: response.data.id, customRadioId })
|
||||
commit('running', true)
|
||||
dispatch('populateQueue', true)
|
||||
}, (response) => {
|
||||
}, () => {
|
||||
logger.error('Error while starting radio', type)
|
||||
})
|
||||
},
|
||||
stop ({ commit, state }) {
|
||||
if (state.current && state.current.clientOnly) {
|
||||
getClientOnlyRadio(state.current).stop()
|
||||
if (state.current?.clientOnly) {
|
||||
CLIENT_RADIOS[state.current.type].stop()
|
||||
}
|
||||
commit('current', null)
|
||||
commit('running', false)
|
||||
|
@ -82,15 +105,17 @@ export default {
|
|||
if (!state.running) {
|
||||
return
|
||||
}
|
||||
|
||||
if (rootState.player.errorCount >= rootState.player.maxConsecutiveErrors - 1) {
|
||||
return
|
||||
}
|
||||
const params = {
|
||||
session: state.current.session
|
||||
}
|
||||
if (state.current.clientOnly) {
|
||||
return getClientOnlyRadio(state.current).populateQueue({ current: state.current, dispatch, state, rootState, playNow })
|
||||
|
||||
const params = { session: state.current?.session }
|
||||
|
||||
if (state.current?.clientOnly) {
|
||||
return CLIENT_RADIOS[state.current.type].populateQueue({ current: state.current, dispatch, playNow })
|
||||
}
|
||||
|
||||
return axios.post('radios/tracks/', params).then((response) => {
|
||||
logger.info('Adding track to queue from radio')
|
||||
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 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,
|
||||
state: {
|
||||
currentLanguage: 'en_US',
|
||||
|
@ -214,7 +273,7 @@ export default {
|
|||
return count
|
||||
},
|
||||
|
||||
windowSize: (state, getters) => {
|
||||
windowSize: (state) => {
|
||||
// IMPORTANT: if you modify these breakpoints, also modify the values in
|
||||
// style/vendor/_media.scss
|
||||
const width = state.window.width
|
||||
|
@ -241,10 +300,10 @@ export default {
|
|||
}
|
||||
},
|
||||
mutations: {
|
||||
addWebsocketEventHandler: (state, { eventName, id, handler }) => {
|
||||
addWebsocketEventHandler: (state, { eventName, id, handler }: { eventName: WebSocketEventName, id: string, handler: () => void}) => {
|
||||
state.websocketEventsHandlers[eventName][id] = handler
|
||||
},
|
||||
removeWebsocketEventHandler: (state, { eventName, id }) => {
|
||||
removeWebsocketEventHandler: (state, { eventName, id }: { eventName: WebSocketEventName, id: string }) => {
|
||||
delete state.websocketEventsHandlers[eventName][id]
|
||||
},
|
||||
currentLanguage: (state, value) => {
|
||||
|
@ -279,10 +338,10 @@ export default {
|
|||
removeMessage (state, key) {
|
||||
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
|
||||
},
|
||||
incrementNotifications (state, { type, count, value }) {
|
||||
incrementNotifications (state, { type, count, value }: { type: NotificationsKey, count: number, value: number }) {
|
||||
if (value !== undefined) {
|
||||
state.notifications[type] = Math.max(0, value)
|
||||
} else {
|
||||
|
@ -292,13 +351,13 @@ export default {
|
|||
pageTitle: (state, value) => {
|
||||
state.pageTitle = value
|
||||
},
|
||||
paginateBy: (state, { route, value }) => {
|
||||
paginateBy: (state, { route, value }: { route: RouteWithPreferences, value: number }) => {
|
||||
state.routePreferences[route].paginateBy = value
|
||||
},
|
||||
ordering: (state, { route, value }) => {
|
||||
ordering: (state, { route, value }: { route: RouteWithPreferences, value: Ordering }) => {
|
||||
state.routePreferences[route].ordering = value
|
||||
},
|
||||
orderingDirection: (state, { route, value }) => {
|
||||
orderingDirection: (state, { route, value }: { route: RouteWithPreferences, value: OrderingDirection }) => {
|
||||
state.routePreferences[route].orderingDirection = value
|
||||
},
|
||||
|
||||
|
@ -307,40 +366,41 @@ export default {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
fetchUnreadNotifications ({ commit }, payload) {
|
||||
fetchUnreadNotifications ({ commit }) {
|
||||
axios.get('federation/inbox/', { params: { is_read: false, page_size: 1 } }).then((response) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
commit('notifications', { type: 'pendingReviewRequests', count: response.data.count })
|
||||
})
|
||||
},
|
||||
|
||||
async currentLanguage ({ state, commit, rootState }, value) {
|
||||
async currentLanguage ({ commit, rootState }, value) {
|
||||
commit('currentLanguage', value)
|
||||
if (rootState.auth.authenticated) {
|
||||
await axios.post('users/settings', { language: value })
|
||||
}
|
||||
},
|
||||
|
||||
websocketEvent ({ state }, event) {
|
||||
websocketEvent ({ state }, event: WebSocketEvent) {
|
||||
const handlers = state.websocketEventsHandlers[event.type]
|
||||
console.log('Dispatching websocket event', event, handlers)
|
||||
if (!handlers) {
|
||||
return
|
||||
}
|
||||
|
||||
const names = Object.keys(handlers)
|
||||
names.forEach((k) => {
|
||||
const handler = handlers[k]
|
||||
|
@ -349,3 +409,5 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default store
|
||||
|
|
|
@ -1,44 +1,45 @@
|
|||
import axios from 'axios'
|
||||
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()
|
||||
|
||||
// import axios from 'axios'
|
||||
|
||||
const RADIOS = {
|
||||
export const CLIENT_RADIOS = {
|
||||
// some radios are client side only, so we have to implement the populateQueue
|
||||
// method by hand
|
||||
account: {
|
||||
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 }
|
||||
axios.get('history/listenings', { params }).then((response) => {
|
||||
axios.get('history/listenings', { params }).then(async (response) => {
|
||||
const latest = response.data.results[0]
|
||||
if (!latest) {
|
||||
logger.error('No more tracks')
|
||||
dispatch('stop')
|
||||
await dispatch('stop')
|
||||
}
|
||||
|
||||
this.offset += 1
|
||||
const append = dispatch('queue/append', { track: latest.track }, { root: true })
|
||||
if (playNow) {
|
||||
append.then(() => {
|
||||
dispatch('queue/last', null, { root: true })
|
||||
})
|
||||
append.then(() => dispatch('queue/last', null, { root: true }))
|
||||
}
|
||||
}, (error) => {
|
||||
}, async (error) => {
|
||||
logger.error('Error while fetching listenings', error)
|
||||
dispatch('stop')
|
||||
await dispatch('stop')
|
||||
})
|
||||
},
|
||||
stop () {
|
||||
this.offset = 1
|
||||
},
|
||||
handleListen (current, event, store) {
|
||||
// XXX: handle actors from other pods
|
||||
handleListen (current: CurrentRadio, event: ListenWSEvent, store: Store<RootState>) {
|
||||
// TODO: handle actors from other pods
|
||||
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) {
|
||||
store.dispatch('queue/append', { track: response.data })
|
||||
await store.dispatch('queue/append', { track: response.data })
|
||||
this.offset += 1
|
||||
}
|
||||
}, (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]
|
||||
}
|
||||
|
||||
export function setCsrf (xhr: XMLHttpRequest) {
|
||||
const token = getCookie('csrftoken')
|
||||
if (token) {
|
||||
xhr.setRequestHeader('X-CSRFToken', token)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (wvffle): Use navigation guards
|
||||
export async function checkRedirectToLogin (store: Store<any>, router: Router) {
|
||||
if (!store.state.auth.authenticated) {
|
||||
|
|
|
@ -236,9 +236,6 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
events: state => state.instance.events
|
||||
}),
|
||||
...mapGetters({
|
||||
additionalNotifications: 'ui/additionalNotifications',
|
||||
showInstanceSupportMessage: 'ui/showInstanceSupportMessage',
|
||||
|
@ -283,21 +280,18 @@ export default {
|
|||
newDisplayDate = null
|
||||
}
|
||||
payload[field] = newDisplayDate
|
||||
const self = this
|
||||
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) {
|
||||
this.isLoading = true
|
||||
const self = this
|
||||
axios.get('federation/inbox/', { params: params }).then(response => {
|
||||
self.isLoading = false
|
||||
self.notifications = response.data
|
||||
this.isLoading = false
|
||||
this.notifications = response.data
|
||||
})
|
||||
},
|
||||
markAllAsRead () {
|
||||
const self = this
|
||||
const before = this.notifications.results[0].id
|
||||
const payload = {
|
||||
action: 'read',
|
||||
|
@ -308,8 +302,8 @@ export default {
|
|||
}
|
||||
}
|
||||
axios.post('federation/inbox/action/', payload).then(response => {
|
||||
self.$store.commit('ui/notifications', { type: 'inbox', count: 0 })
|
||||
self.notifications.results.forEach(n => {
|
||||
this.$store.commit('ui/notifications', { type: 'inbox', count: 0 })
|
||||
this.notifications.results.forEach(n => {
|
||||
n.is_read = true
|
||||
})
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ import LoginForm from '~/components/auth/LoginForm.vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useStore } from 'vuex'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
interface Props {
|
||||
next?: string
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<i
|
||||
role="button"
|
||||
class="close icon"
|
||||
@click="pendingUploads = []"
|
||||
@click="pendingUploads.length = 0"
|
||||
/>
|
||||
<h3 class="ui header">
|
||||
<translate translate-context="Content/Channel/Header">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { humanSize } from '~/utils/filters'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["dom", "esnext", "webworker"],
|
||||
"allowJs": true,
|
||||
|
||||
"noUnusedLocals": 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