import type { Module } from 'vuex' import type { RootState } from '~/store/index' import type { components } from '~/generated/types' import axios from 'axios' import { merge } from 'lodash-es' import useLogger from '~/composables/useLogger' import { useQueue } from '~/composables/audio/queue' import { isTauri } from '~/composables/tauri' export interface State { frontSettings: FrontendSettings instanceUrl?: string knownInstances: string[] nodeinfo: components['schemas']['NodeInfo21'] | null settings: Settings } type TotalCount = { total: number } // export interface NodeInfo { // version: string; // software: { // name: string; // version: string; // } // protocols: any[]; // services?: { // inbound?: string[]; // outbound?: string[]; // } // openRegistrations: boolean; // usage: { // users: { // total: number; // activeHalfyear: number; // activeMonth: number; // } // } // metadata: { // actorId: string // 'private': boolean // shortDescription: string // longDescription: string // rules: string // contactEmail: string // terms: string // nodeName: string // banner: string // defaultUploadQuota: number // content: { // federationEnabled: boolean // anonymousCanListen: boolean // local: { // tracks?: TotalCount // artists?: TotalCount // albums?: TotalCount // hoursOfContent?: number } // } // topMusicCategories: [] // topPodcastCategories: [] // federation: { // followedInstances: number // followingInstances: number // } // supportedUploadExtensions: string[] // allowList: { // enabled: boolean // domains: string[] // } // reportTypes: { // 'type': string // label: string // anonymous: boolean // }[] // funkwhaleSupportMessageEnabled: boolean // instanceSupportMessage: string // endpoints: { // knownNodes?: string // channels?: string // libraries?: string // } // usage: { // favorites: { tracks: TotalCount } // listenings: TotalCount // downloads: TotalCount // } // features:[] // } // } interface FrontendSettings { defaultServerUrl: string additionalStylesheets: string[] } 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 QualityFiltersSettings { [key: string]: any; bitrate_filter: { value: number } has_mbid: { value: boolean } } interface SubsonicSettings { enabled: { value: boolean } } interface UISettings { custom_css: { value: string } } interface Settings { instance: InstanceSettings users: UsersSettings moderation: ModerationSettings quality_filters: QualityFiltersSettings subsonic: SubsonicSettings ui: UISettings } const logger = useLogger() // Use some arbitrary url that will trigger the instance chooser, this needs to be a valid url export const TAURI_DEFAULT_INSTANCE_URL = 'http://localhost/force-instance-chooser/' // We have several way to guess the API server url. By order of precedence: // 0. use the url provided in settings.json, if any. That's a lazy operation done by already initialized store. // 1. force instance chooser, if in tauri app // 2. use the url specified when building via VUE_APP_INSTANCE_URL // 3. use the current url export const findDefaultInstanceUrl = () => { if (isTauri()) { return TAURI_DEFAULT_INSTANCE_URL } try { return new URL(import.meta.env.VUE_APP_INSTANCE_URL as string).href } catch (e) { logger.warn('Invalid VUE_APP_INSTANCE_URL, falling back to current url', e) } return `${location.origin}/` } const DEFAULT_INSTANCE_URL = findDefaultInstanceUrl() const store: Module = { namespaced: true, state: { frontSettings: { defaultServerUrl: DEFAULT_INSTANCE_URL, additionalStylesheets: [] }, instanceUrl: DEFAULT_INSTANCE_URL, knownInstances: [], nodeinfo: null, settings: { instance: { name: { value: '' }, short_description: { value: '' }, long_description: { value: '' }, funkwhale_support_message_enabled: { value: true }, support_message: { value: '' } }, users: { registration_enabled: { value: true }, upload_quota: { value: 0 } }, moderation: { signup_approval_enabled: { value: false }, signup_form_customization: { value: null } }, quality_filters: { bitrate_filter: { value: 0 }, has_mbid: { value: false } }, subsonic: { enabled: { value: true } }, ui: { custom_css: { value: '' } } } }, mutations: { settings: (state, value) => { merge(state.settings, value) }, nodeinfo: (state, value) => { state.nodeinfo = value }, instanceUrl: (state, value) => { try { const { href } = new URL(value) state.instanceUrl = href axios.defaults.baseURL = `${href}api/v2/` // append the URL to the list (and remove existing one if needed) const index = state.knownInstances.indexOf(href) if (index > -1) state.knownInstances.splice(index, 1) state.knownInstances.unshift(href) } catch (e) { logger.error('Invalid instance URL', e) axios.defaults.baseURL = undefined } } }, getters: { absoluteUrl: (_state, getters) => (relativeUrl: string) => { if (relativeUrl.startsWith('http')) return relativeUrl return relativeUrl.startsWith('/') ? `${getters.url.href}${relativeUrl.slice(1)}` : `${getters.url.href}${relativeUrl}` }, url: (state) => new URL(state.instanceUrl ?? DEFAULT_INSTANCE_URL), domain: (_state, getters) => getters.url.hostname, defaultInstance: () => DEFAULT_INSTANCE_URL, qualityFilters: (state) => { const qualityFilters = state.settings.quality_filters const filteredQualityFilters: Record = {} for (const key in qualityFilters) { if (Object.prototype.hasOwnProperty.call(qualityFilters, key)) { if (qualityFilters[key].value === false) { continue } else { filteredQualityFilters[key] = qualityFilters[key].value } } } return filteredQualityFilters } }, actions: { setUrl ({ commit }, url) { commit('instanceUrl', url) const modules = [ 'auth', 'favorites', 'moderation', 'player', 'playlists', 'queue', 'radios' ] modules.forEach(m => { commit(`${m}/reset`, null, { root: true }) }) const { clear } = useQueue() return clear() }, async fetchSettings ({ commit }) { const response = await axios.get('instance/settings/') .catch(err => logger.error('Error while fetching settings', err.response.data)) if (!response?.data || !Array.isArray(response?.data)) return logger.info('Successfully fetched instance settings') type SettingsSection = { section: string, name: string } const sections = response.data.reduce((map: Record>, entry: SettingsSection) => { map[entry.section] ??= {} map[entry.section][entry.name] = entry return map }, {}) commit('settings', sections) }, async fetchFrontSettings ({ state }) { const response = await axios.get(`${import.meta.env.BASE_URL}settings.json`) .catch(() => logger.error('Error when fetching front-end configuration (or no customization available)')) if (!response) return for (const [key, value] of Object.entries(response.data as FrontendSettings)) { if (key === 'defaultServerUrl' && !value) { state.frontSettings.defaultServerUrl = DEFAULT_INSTANCE_URL continue } state.frontSettings[key as keyof FrontendSettings] = value } } } } export default store