Add store types and fix other type errors

This commit is contained in:
Kasper Seweryn 2022-05-03 02:30:43 +02:00 committed by Georg Krause
parent 9e0596d136
commit 0b53ec5b1c
43 changed files with 926 additions and 784 deletions

View File

@ -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",

View File

@ -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)
}
}
})

View File

@ -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'

View File

@ -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 () {

View File

@ -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: {

View File

@ -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'

View File

@ -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

View File

@ -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
})
}

View File

@ -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)
}
})
},

View File

@ -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, {

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useToggle } from '@vueuse/core'
interface Props {

View File

@ -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',

View File

@ -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

View File

@ -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>

View File

@ -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()

View File

@ -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')

View File

@ -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

View File

@ -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
}

View File

@ -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 })

View File

@ -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 {

View File

@ -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
}, {})

View File

@ -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

View File

@ -618,7 +618,7 @@ export default createRouter({
component: () =>
import('~/components/library/Home.vue'),
name: 'library.me',
props: route => ({
props: () => ({
scope: 'me'
})
},

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]
}

View File

@ -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) {

View File

@ -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
})
})

View File

@ -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

View File

@ -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">

View File

@ -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()

View File

@ -13,6 +13,7 @@
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["dom", "esnext", "webworker"],
"allowJs": true,
"noUnusedLocals": true,
"strictNullChecks": true,

File diff suppressed because it is too large Load Diff