Merge branch stable into develop
This commit is contained in:
parent
d2f7d45a0d
commit
ec8dc8e09f
57 changed files with 2844 additions and 1907 deletions
|
|
@ -26,6 +26,9 @@ module.exports = {
|
|||
// NOTE: Nicer for the eye
|
||||
'operator-linebreak': ['error', 'before'],
|
||||
|
||||
// NOTE: We have a logger instance
|
||||
'no-console': 'error',
|
||||
|
||||
// NOTE: Handled by typescript
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
|
|
|
|||
|
|
@ -20,26 +20,25 @@
|
|||
"@sentry/tracing": "7.47.0",
|
||||
"@sentry/vue": "7.47.0",
|
||||
"@vue/runtime-core": "3.3.2",
|
||||
"@vueuse/core": "9.12.0",
|
||||
"@vueuse/integrations": "9.12.0",
|
||||
"@vueuse/math": "9.12.0",
|
||||
"@vueuse/router": "9.12.0",
|
||||
"@vueuse/core": "10.3.0",
|
||||
"@vueuse/integrations": "10.3.0",
|
||||
"@vueuse/math": "10.3.0",
|
||||
"@vueuse/router": "10.3.0",
|
||||
"axios": "1.2.3",
|
||||
"axios-auth-refresh": "3.3.6",
|
||||
"butterchurn": "3.0.0-beta.4",
|
||||
"butterchurn-presets": "3.0.0-beta.4",
|
||||
"diff": "5.1.0",
|
||||
"dompurify": "2.4.5",
|
||||
"dompurify": "2.4.7",
|
||||
"focus-trap": "7.2.0",
|
||||
"fomantic-ui-css": "2.9.2",
|
||||
"howler": "2.2.3",
|
||||
"idb-keyval": "6.2.1",
|
||||
"js-logger": "1.6.1",
|
||||
"lodash-es": "4.17.21",
|
||||
"lru-cache": "7.14.1",
|
||||
"moment": "2.29.4",
|
||||
"showdown": "2.1.0",
|
||||
"standardized-audio-context": "25.3.53",
|
||||
"standardized-audio-context": "25.3.55",
|
||||
"text-clipper": "2.2.0",
|
||||
"transliteration": "2.3.5",
|
||||
"universal-cookie": "4.0.4",
|
||||
|
|
@ -50,7 +49,7 @@
|
|||
"vue-upload-component": "3.1.8",
|
||||
"vue-virtual-scroller": "2.0.0-beta.8",
|
||||
"vue3-gettext": "2.3.4",
|
||||
"vue3-lazyload": "0.3.6",
|
||||
"vue3-lazyload": "0.3.8",
|
||||
"vuedraggable": "4.1.0",
|
||||
"vuex": "4.1.0",
|
||||
"vuex-persistedstate": "4.1.0",
|
||||
|
|
@ -72,6 +71,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "5.48.2",
|
||||
"@vitejs/plugin-vue": "4.2.3",
|
||||
"@vitest/coverage-c8": "0.25.8",
|
||||
"@vue-macros/volar": "0.13.3",
|
||||
"@vue/compiler-sfc": "3.3.2",
|
||||
"@vue/eslint-config-standard": "8.0.1",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
|
|
@ -94,6 +94,7 @@
|
|||
"sinon": "15.0.2",
|
||||
"standardized-audio-context-mock": "9.6.18",
|
||||
"typescript": "4.9.5",
|
||||
"unplugin-vue-macros": "2.4.6",
|
||||
"utility-types": "3.10.0",
|
||||
"vite": "4.3.5",
|
||||
"vite-plugin-pwa": "0.14.4",
|
||||
|
|
|
|||
|
|
@ -267,7 +267,11 @@ REPLACEMENTS = {
|
|||
("background", "var(--site-background)"),
|
||||
("color", "var(--text-color)"),
|
||||
],
|
||||
("::-webkit-selection", "::-moz-selection", "::selection"): [
|
||||
(
|
||||
"::-webkit-selection",
|
||||
"::-moz-selection",
|
||||
"::selection",
|
||||
): [
|
||||
("color", "var(--text-selection-color)"),
|
||||
("background-color", "var(--text-selection-background)"),
|
||||
],
|
||||
|
|
@ -448,7 +452,10 @@ REPLACEMENTS = {
|
|||
): [
|
||||
("color", "var(--input-focus-placeholder-color)"),
|
||||
],
|
||||
(".ui.form .field > label", ".ui.form .inline.fields .field > label"): [
|
||||
(
|
||||
".ui.form .field > label",
|
||||
".ui.form .inline.fields .field > label",
|
||||
): [
|
||||
("color", "var(--form-label-color)"),
|
||||
],
|
||||
},
|
||||
|
|
@ -779,7 +786,10 @@ REPLACEMENTS = {
|
|||
".structured",
|
||||
"tablet stackable",
|
||||
],
|
||||
(".ui.table", ".ui.table > thead > tr > th"): [
|
||||
(
|
||||
".ui.table",
|
||||
".ui.table > thead > tr > th",
|
||||
): [
|
||||
("color", "var(--text-color)"),
|
||||
("background", "var(--table-background)"),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import { createEventHook, refDefault, type EventHookOn, useEventListener } from
|
|||
import { createAudioSource } from '~/composables/audio/audio-api'
|
||||
import { effectScope, reactive, ref, type Ref } from 'vue'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
export interface SoundSource {
|
||||
uuid: string
|
||||
mimetype: string
|
||||
|
|
@ -73,7 +77,7 @@ export class HTMLSound implements Sound {
|
|||
this.#audio.src = source
|
||||
this.#audio.preload = 'auto'
|
||||
|
||||
console.log('CREATED SOUND INSTANCE', this)
|
||||
logger.log('CREATED SOUND INSTANCE', this)
|
||||
|
||||
this.#scope.run(() => {
|
||||
useEventListener(this.#audio, 'ended', () => this.#soundEndEventHook.trigger(this))
|
||||
|
|
@ -84,19 +88,19 @@ export class HTMLSound implements Sound {
|
|||
})
|
||||
|
||||
useEventListener(this.#audio, 'waiting', () => {
|
||||
console.log('>> AUDIO WAITING', this)
|
||||
logger.log('>> AUDIO WAITING', this)
|
||||
})
|
||||
|
||||
useEventListener(this.#audio, 'playing', () => {
|
||||
console.log('>> AUDIO PLAYING', this)
|
||||
logger.log('>> AUDIO PLAYING', this)
|
||||
})
|
||||
|
||||
useEventListener(this.#audio, 'stalled', () => {
|
||||
console.log('>> AUDIO STALLED', this)
|
||||
logger.log('>> AUDIO STALLED', this)
|
||||
})
|
||||
|
||||
useEventListener(this.#audio, 'suspend', () => {
|
||||
console.log('>> AUDIO SUSPEND', this)
|
||||
logger.log('>> AUDIO SUSPEND', this)
|
||||
})
|
||||
|
||||
useEventListener(this.#audio, 'loadeddata', () => {
|
||||
|
|
@ -106,7 +110,7 @@ export class HTMLSound implements Sound {
|
|||
|
||||
useEventListener(this.#audio, 'error', (err) => {
|
||||
if (this.#ignoreError) return
|
||||
console.error('>> AUDIO ERRORED', err, this)
|
||||
logger.error('>> AUDIO ERRORED', err, this)
|
||||
this.isErrored.value = true
|
||||
this.isLoaded.value = true
|
||||
})
|
||||
|
|
@ -116,7 +120,7 @@ export class HTMLSound implements Sound {
|
|||
async preload () {
|
||||
this.isDisposed.value = false
|
||||
this.isErrored.value = false
|
||||
console.log('CALLING PRELOAD ON', this)
|
||||
logger.log('CALLING PRELOAD ON', this)
|
||||
this.#audio.load()
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +145,7 @@ export class HTMLSound implements Sound {
|
|||
try {
|
||||
await this.#audio.play()
|
||||
} catch (err) {
|
||||
console.error('>> AUDIO PLAY ERROR', err, this)
|
||||
logger.error('>> AUDIO PLAY ERROR', err, this)
|
||||
this.isErrored.value = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
|||
import LoginForm from '~/components/auth/LoginForm.vue'
|
||||
import SignupForm from '~/components/auth/SignupForm.vue'
|
||||
import useMarkdown from '~/composables/useMarkdown'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import { humanSize } from '~/utils/filters'
|
||||
import { useStore } from '~/store'
|
||||
import { computed } from 'vue'
|
||||
|
|
@ -18,6 +19,7 @@ const labels = computed(() => ({
|
|||
}))
|
||||
|
||||
const store = useStore()
|
||||
const logger = useLogger()
|
||||
const nodeinfo = computed(() => store.state.instance.nodeinfo)
|
||||
|
||||
const podName = computed(() => get(nodeinfo.value, 'metadata.nodeName') || 'Funkwhale')
|
||||
|
|
@ -54,7 +56,7 @@ const headerStyle = computed(() => {
|
|||
// TODO (wvffle): Check if needed
|
||||
const router = useRouter()
|
||||
whenever(() => store.state.auth.authenticated, () => {
|
||||
console.log('Authenticated, redirecting to /library…')
|
||||
logger.log('Authenticated, redirecting to /library…')
|
||||
router.push('/library')
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useStore } from '~/store'
|
|||
import axios from 'axios'
|
||||
|
||||
import updateQueryString from '~/composables/updateQueryString'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
type Type = 'rss' | 'artists' | 'both'
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ const type = ref(props.initialType)
|
|||
const id = ref(props.initialId)
|
||||
const errors = ref([] as string[])
|
||||
|
||||
const logger = useLogger()
|
||||
const { t } = useI18n()
|
||||
const labels = computed(() => ({
|
||||
title: type.value === 'rss'
|
||||
|
|
@ -102,7 +104,7 @@ const submit = () => {
|
|||
|
||||
const isLoading = ref(false)
|
||||
const createFetch = async () => {
|
||||
console.log(id.value, props.standalone)
|
||||
logger.debug(id.value, props.standalone)
|
||||
if (!id.value) return
|
||||
if (props.standalone) {
|
||||
history.replaceState(history.state, '', updateQueryString(location.href, 'id', id.value))
|
||||
|
|
|
|||
|
|
@ -18,15 +18,16 @@ const values = reactive({} as Record<string, unknown | Form | string>)
|
|||
const result = ref<boolean | null>(null)
|
||||
const errors = ref([] as string[])
|
||||
|
||||
const fileRefs = reactive({} as Record<string, HTMLInputElement>)
|
||||
const setFileRef = (identifier: string) => (el: FunctionRef) => {
|
||||
console.log(el)
|
||||
fileRefs[identifier] = el as HTMLInputElement
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
const store = useStore()
|
||||
|
||||
// TODO (wvffle): Use VueUse
|
||||
const fileRefs = reactive({} as Record<string, HTMLInputElement>)
|
||||
const setFileRef = (identifier: string) => (el: FunctionRef) => {
|
||||
logger.debug(el)
|
||||
fileRefs[identifier] = el as HTMLInputElement
|
||||
}
|
||||
|
||||
const settings = computed(() => {
|
||||
const byIdentifier = props.settingsData.reduce((acc, entry) => {
|
||||
acc[entry.identifier] = entry
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ watch(page, fetchData, { immediate: true })
|
|||
<podcast-table
|
||||
v-if="isPodcast"
|
||||
v-model:page="page"
|
||||
:paginate-by="limit"
|
||||
:default-cover="defaultCover"
|
||||
:is-podcast="isPodcast"
|
||||
:show-art="true"
|
||||
|
|
@ -80,7 +81,6 @@ watch(page, fetchData, { immediate: true })
|
|||
:show-album="false"
|
||||
:paginate-results="true"
|
||||
:total="count"
|
||||
:paginate-by="limit"
|
||||
/>
|
||||
<track-table
|
||||
v-else
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import PlayButton from '~/components/audio/PlayButton.vue'
|
|||
import useMarkdown from '~/composables/useMarkdown'
|
||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import SanitizedHtml from '~/components/SanitizedHtml.vue'
|
||||
|
||||
interface Props extends PlayOptionsProps {
|
||||
tracks: Track[]
|
||||
|
|
@ -81,7 +82,7 @@ await fetchData()
|
|||
@click.prevent.exact="activateTrack(track, index)"
|
||||
>
|
||||
<img
|
||||
v-if="track.cover?.urls.original "
|
||||
v-if="track.cover?.urls.original"
|
||||
v-lazy="$store.getters['instance/absoluteUrl'](track.cover.urls.medium_square_crop)"
|
||||
alt=""
|
||||
class="ui artist-track mini image"
|
||||
|
|
@ -111,7 +112,7 @@ await fetchData()
|
|||
v-if="renderedDescription"
|
||||
class="podcast-episode-meta"
|
||||
>
|
||||
{{ renderedDescription }}
|
||||
<SanitizedHtml :html="renderedDescription" />
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ interface Props {
|
|||
isPodcast?: boolean
|
||||
paginateResults?: boolean
|
||||
paginateBy?: number
|
||||
page?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
|
|
@ -30,9 +29,10 @@ withDefaults(defineProps<Props>(), {
|
|||
isPodcast: true,
|
||||
paginateResults: true,
|
||||
paginateBy: 25,
|
||||
page: 1,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const { page } = defineModels<{ page: number, }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -44,9 +44,7 @@ withDefaults(defineProps<Props>(), {
|
|||
<slot name="header" />
|
||||
|
||||
<div>
|
||||
<div
|
||||
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
||||
>
|
||||
<div :class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']">
|
||||
<!-- For each item, build a row -->
|
||||
<podcast-row
|
||||
v-for="(track, index) in tracks"
|
||||
|
|
@ -65,8 +63,8 @@ withDefaults(defineProps<Props>(), {
|
|||
>
|
||||
<pagination
|
||||
v-bind="$attrs"
|
||||
v-model:current="page"
|
||||
:total="total"
|
||||
:current="page"
|
||||
:paginate-by="paginateBy"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -95,8 +93,8 @@ withDefaults(defineProps<Props>(), {
|
|||
<pagination
|
||||
v-if="paginateResults"
|
||||
v-bind="$attrs"
|
||||
v-model:current="page"
|
||||
:total="total"
|
||||
:current="page"
|
||||
:compact="true"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { useStore } from '~/store'
|
|||
|
||||
import ApplicationForm from '~/components/auth/ApplicationForm.vue'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
interface Props {
|
||||
name?: string
|
||||
scopes?: string
|
||||
|
|
@ -27,6 +29,7 @@ const defaults = reactive({
|
|||
redirectUris: props.redirectUris
|
||||
})
|
||||
|
||||
const logger = useLogger()
|
||||
const { t } = useI18n()
|
||||
const labels = computed(() => ({
|
||||
title: t('components.auth.ApplicationNew.title')
|
||||
|
|
@ -37,7 +40,7 @@ const store = useStore()
|
|||
|
||||
const created = (application: Application) => {
|
||||
store.state.auth.applicationSecret = application.client_secret
|
||||
console.log(application)
|
||||
logger.debug(application)
|
||||
return router.push({
|
||||
name: 'settings.applications.edit',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const labels = computed(() => ({
|
|||
|
||||
const signupRequiresApproval = computed(() => props.signupApprovalEnabled ?? store.state.instance.settings.moderation.signup_approval_enabled.value)
|
||||
const formCustomization = computed(() => props.customization ?? store.state.instance.settings.moderation.signup_form_customization.value)
|
||||
watchEffect(() => console.log(store.state.instance.settings.moderation.signup_approval_enabled.value))
|
||||
watchEffect(() => logger.debug(store.state.instance.settings.moderation.signup_approval_enabled.value))
|
||||
|
||||
const payload = reactive({
|
||||
username: '',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { useVModel, watchDebounced, useTextareaAutosize, syncRef } from '@vueuse
|
|||
import { ref, computed, watchEffect, onMounted, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
interface Events {
|
||||
(e: 'update:modelValue', value: string): void
|
||||
}
|
||||
|
|
@ -26,6 +28,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
required: false
|
||||
})
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { textarea, input } = useTextareaAutosize()
|
||||
const value = useVModel(props, 'modelValue', emit)
|
||||
|
|
@ -47,7 +51,7 @@ const loadPreview = async () => {
|
|||
const response = await axios.post('text-preview/', { text: value.value, permissive: props.permissive })
|
||||
preview.value = response.data.rendered
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
logger.error(error)
|
||||
}
|
||||
isLoadingPreview.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ const fetchFavorites = async () => {
|
|||
ordering: orderingString.value
|
||||
}
|
||||
|
||||
const measureLoading = logger.time('Loading user favorites')
|
||||
try {
|
||||
logger.time('Loading user favorites')
|
||||
const response = await axios.get('tracks/', { params })
|
||||
|
||||
results.length = 0
|
||||
|
|
@ -81,7 +81,7 @@ const fetchFavorites = async () => {
|
|||
} catch (error) {
|
||||
useErrorHandler(error as Error)
|
||||
} finally {
|
||||
logger.timeEnd('Loading user favorites')
|
||||
measureLoading()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import TagsList from '~/components/tags/List.vue'
|
|||
import AlbumDropdown from './AlbumDropdown.vue'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
interface Events {
|
||||
(e: 'deleted'): void
|
||||
|
|
@ -39,6 +40,8 @@ const isSerie = computed(() => object.value?.artist.content_category === 'podcas
|
|||
const totalDuration = computed(() => sum((object.value?.tracks ?? []).map(track => track.uploads[0]?.duration ?? 0)))
|
||||
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const { t } = useI18n()
|
||||
const labels = computed(() => ({
|
||||
title: t('components.library.AlbumBase.title')
|
||||
|
|
@ -93,7 +96,7 @@ const fetchTracks = async () => {
|
|||
tracks.push(...response.data.results)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
logger.error(error)
|
||||
} finally {
|
||||
isLoadingTracks.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const fetchData = async () => {
|
|||
content_category: 'music'
|
||||
}
|
||||
|
||||
logger.time('Fetching albums')
|
||||
const measureLoading = logger.time('Fetching albums')
|
||||
try {
|
||||
const response = await axios.get('albums/', {
|
||||
params,
|
||||
|
|
@ -86,7 +86,7 @@ const fetchData = async () => {
|
|||
useErrorHandler(error as Error)
|
||||
result.value = undefined
|
||||
} finally {
|
||||
logger.timeEnd('Fetching albums')
|
||||
measureLoading()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const fetchData = async () => {
|
|||
has_albums: excludeCompilation.value
|
||||
}
|
||||
|
||||
logger.time('Fetching artists')
|
||||
const measureLoading = logger.time('Fetching artists')
|
||||
try {
|
||||
const response = await axios.get('artists/', {
|
||||
params,
|
||||
|
|
@ -87,7 +87,7 @@ const fetchData = async () => {
|
|||
useErrorHandler(error as Error)
|
||||
result.value = undefined
|
||||
} finally {
|
||||
logger.timeEnd('Fetching artists')
|
||||
measureLoading()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const labels = computed(() => ({
|
|||
const isLoading = ref(false)
|
||||
const fetchData = async () => {
|
||||
isLoading.value = true
|
||||
logger.time('Loading latest artists')
|
||||
const measureLoading = logger.time('Loading latest artists')
|
||||
|
||||
const params = {
|
||||
ordering: '-creation_date',
|
||||
|
|
@ -47,7 +47,7 @@ const fetchData = async () => {
|
|||
}
|
||||
|
||||
isLoading.value = false
|
||||
logger.timeEnd('Loading latest artists')
|
||||
measureLoading()
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ const fetchData = async () => {
|
|||
content_category: 'podcast'
|
||||
}
|
||||
|
||||
logger.time('Fetching podcasts')
|
||||
const measureLoading = logger.time('Fetching podcasts')
|
||||
try {
|
||||
const response = await axios.get('artists/', {
|
||||
params,
|
||||
|
|
@ -88,7 +88,7 @@ const fetchData = async () => {
|
|||
useErrorHandler(error as Error)
|
||||
result.value = undefined
|
||||
} finally {
|
||||
logger.timeEnd('Fetching podcasts')
|
||||
measureLoading()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const fetchData = async () => {
|
|||
ordering: orderingString.value
|
||||
}
|
||||
|
||||
logger.time('Fetching radios')
|
||||
const measureLoading = logger.time('Fetching radios')
|
||||
try {
|
||||
const response = await axios.get('radios/radios/', {
|
||||
params
|
||||
|
|
@ -75,7 +75,7 @@ const fetchData = async () => {
|
|||
useErrorHandler(error as Error)
|
||||
result.value = undefined
|
||||
} finally {
|
||||
logger.timeEnd('Fetching radios')
|
||||
measureLoading()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import NoteForm from '~/components/manage/moderation/NoteForm.vue'
|
|||
import useReportConfigs from '~/composables/moderation/useReportConfigs'
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import useMarkdown from '~/composables/useMarkdown'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
interface Events {
|
||||
(e: 'updated', updating: { type: string }): void
|
||||
|
|
@ -29,6 +30,7 @@ const emit = defineEmits<Events>()
|
|||
const props = defineProps<Props>()
|
||||
|
||||
const configs = useReportConfigs()
|
||||
const logger = useLogger()
|
||||
|
||||
const obj = ref(props.initObj)
|
||||
const summary = useMarkdown(() => obj.value.summary ?? '')
|
||||
|
|
@ -77,11 +79,11 @@ const actions = computed(() => {
|
|||
handler: async () => {
|
||||
try {
|
||||
await axios.delete(deleteUrl)
|
||||
console.log('Target deleted')
|
||||
logger.info('Target deleted')
|
||||
obj.value.target = undefined
|
||||
resolveReport(true)
|
||||
} catch (error) {
|
||||
console.log('Error while deleting target', error)
|
||||
logger.error('Error while deleting target', error)
|
||||
useErrorHandler(error as Error)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { useStore } from '~/store'
|
|||
import axios from 'axios'
|
||||
import { useClamp } from '@vueuse/math'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
// Looping
|
||||
export enum LoopingMode {
|
||||
None,
|
||||
|
|
@ -17,7 +19,6 @@ export enum LoopingMode {
|
|||
}
|
||||
|
||||
// Pausing
|
||||
|
||||
export enum PauseReason {
|
||||
UserInput,
|
||||
EndOfQueue,
|
||||
|
|
@ -26,6 +27,8 @@ export enum PauseReason {
|
|||
EndOfRadio
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const MODE_MAX = 1 + Math.max(...Object.values(LoopingMode).filter(mode => typeof mode === 'number') as number[])
|
||||
|
||||
export const looping: Ref<number> = useStorage('player:looping', LoopingMode.None)
|
||||
|
|
@ -131,12 +134,12 @@ export const usePlayer = createGlobalState(() => {
|
|||
const trackListenSubmissions = () => {
|
||||
const store = useStore()
|
||||
whenever(listenSubmitted, async () => {
|
||||
console.log('Listening submitted!')
|
||||
logger.log('Listening submitted!')
|
||||
if (!store.state.auth.authenticated) return
|
||||
if (!currentTrack.value) return
|
||||
|
||||
await axios.post('history/listenings/', { track: currentTrack.value.id })
|
||||
.catch((error) => console.error('Could not record track in history', error))
|
||||
.catch((error) => logger.error('Could not record track in history', error))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { delMany, getMany, setMany } from '~/composables/data/indexedDB'
|
|||
import { setGain } from '~/composables/audio/audio-api'
|
||||
import { useTracks } from '~/composables/audio/tracks'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
import axios from 'axios'
|
||||
|
||||
export interface QueueTrackSource {
|
||||
|
|
@ -36,6 +38,8 @@ export interface QueueTrack {
|
|||
sources: QueueTrackSource[]
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
// Queue
|
||||
const tracks = useStorage('queue:tracks', [] as number[])
|
||||
const shuffledIds = useStorage('queue:tracks:shuffled', [] as number[])
|
||||
|
|
@ -69,7 +73,7 @@ watchEffect(async () => {
|
|||
tracksById.set(track.id, track)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
logger.error(error)
|
||||
} finally {
|
||||
fetchingTracks.value = false
|
||||
}
|
||||
|
|
@ -329,6 +333,10 @@ export const useQueue = createGlobalState(() => {
|
|||
clearRadio.value = true
|
||||
|
||||
const lastTracks = [...tracks.value]
|
||||
|
||||
// Clear shuffled tracks
|
||||
shuffledIds.value.length = 0
|
||||
|
||||
tracks.value.length = 0
|
||||
await delMany(lastTracks)
|
||||
|
||||
|
|
@ -340,7 +348,7 @@ export const useQueue = createGlobalState(() => {
|
|||
const store = useStore()
|
||||
watchEffect(() => {
|
||||
if (store.state.radios.running && currentIndex.value === tracks.value.length - 1) {
|
||||
console.log('POPULATING QUEUE FROM RADIO')
|
||||
logger.log('POPULATING QUEUE FROM RADIO')
|
||||
return store.dispatch('radios/populateQueue')
|
||||
}
|
||||
})
|
||||
|
|
@ -375,7 +383,7 @@ export const useQueue = createGlobalState(() => {
|
|||
|
||||
currentIndex.value = index
|
||||
delete localStorage.queue
|
||||
})().catch((error) => console.error('Could not successfully migrate between queue versions', error))
|
||||
})().catch((error) => logger.error('Could not successfully migrate between queue versions', error))
|
||||
}
|
||||
|
||||
if (localStorage.player) {
|
||||
|
|
@ -385,7 +393,7 @@ export const useQueue = createGlobalState(() => {
|
|||
setGain(volume ?? 0.7)
|
||||
delete localStorage.player
|
||||
} catch (error) {
|
||||
console.error('Could not successfully migrate between player versions', error)
|
||||
logger.error('Could not successfully migrate between player versions', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { useQueue } from '~/composables/audio/queue'
|
|||
import { soundImplementation } from '~/api/player'
|
||||
|
||||
import useLRUCache from '~/composables/data/useLRUCache'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import store from '~/store'
|
||||
|
||||
import axios from 'axios'
|
||||
|
|
@ -18,6 +19,8 @@ import axios from 'axios'
|
|||
const ALLOWED_PLAY_TYPES: (CanPlayTypeResult | undefined)[] = ['maybe', 'probably']
|
||||
const AUDIO_ELEMENT = document.createElement('audio')
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const soundPromises = new Map<number, Promise<Sound>>()
|
||||
const soundCache = useLRUCache<number, Sound>({
|
||||
max: 3,
|
||||
|
|
@ -95,7 +98,7 @@ export const useTracks = createGlobalState(() => {
|
|||
const sound = new SoundImplementation(sources)
|
||||
|
||||
sound.onSoundEnd(() => {
|
||||
console.log('TRACK ENDED, PLAYING NEXT')
|
||||
logger.log('TRACK ENDED, PLAYING NEXT')
|
||||
|
||||
// NOTE: We push it to the end of the job queue
|
||||
setTimeout(() => playNext(), 0)
|
||||
|
|
@ -140,7 +143,7 @@ export const useTracks = createGlobalState(() => {
|
|||
return sound
|
||||
}
|
||||
|
||||
console.log('NO TRACK IN CACHE, CREATING', track)
|
||||
logger.log('NO TRACK IN CACHE, CREATING', track)
|
||||
const soundPromise = createSoundPromise()
|
||||
soundPromises.set(track.id, soundPromise)
|
||||
return soundPromise
|
||||
|
|
@ -171,7 +174,7 @@ export const useTracks = createGlobalState(() => {
|
|||
|
||||
const { queue, currentIndex } = useQueue()
|
||||
if (queue.value.length <= index || index === -1) return
|
||||
console.log('LOADING TRACK', index)
|
||||
logger.log('LOADING TRACK', index)
|
||||
|
||||
const track = queue.value[index]
|
||||
const sound = await createSound(track)
|
||||
|
|
@ -181,7 +184,7 @@ export const useTracks = createGlobalState(() => {
|
|||
return
|
||||
}
|
||||
|
||||
console.log('CONNECTING NODE', sound)
|
||||
logger.log('CONNECTING NODE', sound)
|
||||
|
||||
sound.audioNode.disconnect()
|
||||
connectAudioSource(sound.audioNode)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { AUDIO_CONTEXT, GAIN_NODE } from './audio-api'
|
|||
import { useResizeObserver, useStorage } from '@vueuse/core'
|
||||
import { watchEffect, ref, markRaw } from 'vue'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
// @ts-expect-error butterchurn has no typings
|
||||
import butterchurnPresets from 'butterchurn-presets'
|
||||
|
||||
|
|
@ -11,6 +13,8 @@ import butterchurnPresets from 'butterchurn-presets'
|
|||
import butterchurn from 'butterchurn'
|
||||
|
||||
export const useMilkDrop = (canvas: Ref<HTMLCanvasElement>) => {
|
||||
const logger = useLogger()
|
||||
|
||||
const presets = Object.keys(butterchurnPresets)
|
||||
const visualizer = ref()
|
||||
|
||||
|
|
@ -24,7 +28,7 @@ export const useMilkDrop = (canvas: Ref<HTMLCanvasElement>) => {
|
|||
const name = presetName.value
|
||||
if (name === undefined) return
|
||||
|
||||
console.log(`Switching to preset: '${name}'`)
|
||||
logger.log(`Switching to preset: '${name}'`)
|
||||
visualizer.value?.loadPreset(butterchurnPresets[name], 1)
|
||||
})
|
||||
|
||||
|
|
@ -67,7 +71,7 @@ export const useMilkDrop = (canvas: Ref<HTMLCanvasElement>) => {
|
|||
try {
|
||||
visualizer.value?.render()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
logger.error(error)
|
||||
loadRandomPreset()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,71 +3,73 @@ import type { RouteRecordName } from 'vue-router'
|
|||
import { toRefs, useStorage, syncRef } from '@vueuse/core'
|
||||
import { useRouteQuery } from '@vueuse/router'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ref, watch } from 'vue'
|
||||
import { watch, readonly } from 'vue'
|
||||
|
||||
export interface OrderingProps {
|
||||
orderingConfigName?: RouteRecordName
|
||||
}
|
||||
|
||||
export default (props: OrderingProps) => {
|
||||
export default <T extends string = string>(props: OrderingProps) => {
|
||||
const route = useRoute()
|
||||
|
||||
const preferences = useStorage(`route-preferences:${props.orderingConfigName?.toString() ?? route.name?.toString() ?? '*'}`, {
|
||||
interface Preferences {
|
||||
orderingDirection: '-' | '+'
|
||||
ordering: T
|
||||
paginateBy: number
|
||||
}
|
||||
|
||||
const preferences = useStorage<Preferences>(`route-preferences:${props.orderingConfigName?.toString() ?? route.name?.toString() ?? '*'}`, () => ({
|
||||
orderingDirection: route.meta.orderingDirection ?? '-',
|
||||
ordering: route.meta.ordering ?? 'creation_date',
|
||||
paginateBy: route.meta.paginateBy ?? 50
|
||||
})
|
||||
}))
|
||||
|
||||
const {
|
||||
orderingDirection: perfOrderingDirection,
|
||||
paginateBy: perfPaginateBy,
|
||||
ordering: perfOrdering
|
||||
} = toRefs(preferences)
|
||||
|
||||
const queryPaginateBy = useRouteQuery<string>('paginateBy', perfPaginateBy.value.toString())
|
||||
const paginateBy = ref()
|
||||
syncRef(queryPaginateBy, paginateBy, {
|
||||
transform: {
|
||||
ltr: (left) => +left,
|
||||
rtl: (right) => right.toString()
|
||||
}
|
||||
orderingDirection: prefOrderingDirection,
|
||||
paginateBy: prefPaginateBy,
|
||||
ordering: prefOrdering
|
||||
} = toRefs(preferences, {
|
||||
replaceRef: false
|
||||
})
|
||||
|
||||
const queryOrdering = useRouteQuery('ordering', perfOrderingDirection.value + perfOrdering.value)
|
||||
console.log(queryOrdering.value)
|
||||
const normalizeDirection = (direction: string) => direction === '+' ? '' : '-'
|
||||
|
||||
const queryOrdering = useRouteQuery(
|
||||
'ordering',
|
||||
normalizeDirection(prefOrderingDirection.value) + prefOrdering.value,
|
||||
{ transform: (value) => value.trim() }
|
||||
)
|
||||
|
||||
const queryPaginateBy = useRouteQuery('paginateBy', prefPaginateBy.value, {
|
||||
transform: Number
|
||||
})
|
||||
|
||||
// NOTE: Sync paginateBy in query string and preferences. We're using `flush: 'post'` to make sure that we sync after all updates are done
|
||||
syncRef(queryPaginateBy, prefPaginateBy, {
|
||||
flush: 'post'
|
||||
})
|
||||
|
||||
// NOTE: Sync ordering from preferences to query string
|
||||
watch([prefOrderingDirection, prefOrdering], () => {
|
||||
queryOrdering.value = normalizeDirection(prefOrderingDirection.value) + prefOrdering.value.trim()
|
||||
})
|
||||
|
||||
// NOTE: Sync ordering from query string to preferences
|
||||
watch(queryOrdering, (ordering) => {
|
||||
perfOrderingDirection.value = ordering[0] === '-' ? '-' : '+'
|
||||
perfOrdering.value = ordering[0] === '-' || ordering[0] === '+'
|
||||
? ordering.slice(1)
|
||||
: ordering
|
||||
prefOrderingDirection.value = ordering[0] === '-' ? '-' : '+'
|
||||
prefOrdering.value = ordering.replace(/^[+-]/, '')
|
||||
}, { immediate: true })
|
||||
|
||||
// NOTE: We're using `flush: 'post'` to make sure that the `onOrderingUpdate` callback is called after all updates are done
|
||||
const onOrderingUpdate = (fn: () => void) => watch(preferences, fn, {
|
||||
flush: 'post'
|
||||
})
|
||||
|
||||
watch(perfOrderingDirection, (direction) => {
|
||||
if (direction === '-') {
|
||||
queryOrdering.value = direction + perfOrdering.value
|
||||
return
|
||||
}
|
||||
|
||||
queryOrdering.value = perfOrdering.value
|
||||
})
|
||||
|
||||
watch(perfOrdering, (field) => {
|
||||
const direction = perfOrderingDirection.value
|
||||
queryOrdering.value = (direction === '-' ? '-' : '') + field
|
||||
})
|
||||
|
||||
watch(queryPaginateBy, (paginateBy) => {
|
||||
perfPaginateBy.value = +paginateBy
|
||||
})
|
||||
|
||||
const onOrderingUpdate = (fn: () => void) => watch(preferences, fn)
|
||||
|
||||
return {
|
||||
paginateBy,
|
||||
ordering: perfOrdering,
|
||||
orderingDirection: perfOrderingDirection,
|
||||
orderingString: queryOrdering,
|
||||
paginateBy: prefPaginateBy,
|
||||
ordering: prefOrdering,
|
||||
orderingDirection: prefOrderingDirection,
|
||||
orderingString: readonly(queryOrdering),
|
||||
onOrderingUpdate
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { DefaultMagicKeysAliasMap, tryOnScopeDispose, useEventListener } from '@
|
|||
import { isEqual, isMatch } from 'lodash-es'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import useLogger from './useLogger'
|
||||
|
||||
type KeyFilter = string | string[]
|
||||
|
||||
interface Entry {
|
||||
|
|
@ -10,6 +12,7 @@ interface Entry {
|
|||
__location?: string
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
const combinations = reactive(new Map())
|
||||
|
||||
const current = new Set()
|
||||
|
|
@ -59,7 +62,7 @@ export default (key: KeyFilter, handler: () => unknown, prevent = false) => {
|
|||
}
|
||||
|
||||
if (collisions.length) {
|
||||
console.warn([
|
||||
logger.warn([
|
||||
'onKeyboardShortcut detected a possible collision in:',
|
||||
`${entry.__location}: ${combination.join(' + ')}`,
|
||||
...collisions
|
||||
|
|
|
|||
|
|
@ -1,11 +1,112 @@
|
|||
import Logger from 'js-logger'
|
||||
type LogLevel = 'info' | 'warn' | 'error' | 'debug' | 'time'
|
||||
|
||||
Logger.useDefaults({
|
||||
defaultLevel: import.meta.env.DEV
|
||||
? Logger.DEBUG
|
||||
: Logger.WARN
|
||||
})
|
||||
const LOG_LEVEL_LABELS: Record<LogLevel, string> = {
|
||||
info: ' INFO',
|
||||
warn: ' WARN',
|
||||
error: 'ERROR',
|
||||
debug: 'DEBUG',
|
||||
time: ' TIME'
|
||||
}
|
||||
|
||||
export default (logger?: string) => logger
|
||||
? Logger.get(logger)
|
||||
: Logger
|
||||
const LOG_LEVEL_BACKGROUND: Record<LogLevel, string> = {
|
||||
info: '#a6e22e',
|
||||
warn: '#FF9800',
|
||||
error: '#F44336',
|
||||
debug: '#00BCD4',
|
||||
time: '#00BCD4'
|
||||
}
|
||||
|
||||
const LOG_LEVEL_COLOR: Record<LogLevel, string> = {
|
||||
info: '#000',
|
||||
warn: '#000',
|
||||
error: '#fff',
|
||||
debug: '#000',
|
||||
time: '#000'
|
||||
}
|
||||
|
||||
const TIMESTAMP_COLOR = '#9E9E9E'
|
||||
|
||||
const FILETYPE_BACKGROUND: Record<string, string> = {
|
||||
js: '#f1e05a',
|
||||
ts: '#2b7489',
|
||||
vue: '#41b883',
|
||||
html: '#e34c26',
|
||||
default: '#ccc'
|
||||
}
|
||||
|
||||
const FILETYPE_COLOR: Record<string, string> = {
|
||||
js: '#000',
|
||||
ts: '#fff',
|
||||
vue: '#fff',
|
||||
html: '#fff',
|
||||
default: '#000'
|
||||
}
|
||||
|
||||
const getFile = () => {
|
||||
const { stack } = new Error()
|
||||
const line = stack?.split('\n')[2] ?? ''
|
||||
const [, method, url, lineNo] = line.match(/^(\w+)?(?:\/<)*@(.+?)(?:\?.*)?:(\d+):\d+$/) ?? []
|
||||
const file = url.startsWith(location.origin) ? url.slice(location.origin.length) : url
|
||||
return { method, file, lineNo }
|
||||
}
|
||||
|
||||
// NOTE: We're pushing all logs to the end of the event loop
|
||||
const createLoggerFn = (level: LogLevel) => {
|
||||
// NOTE: We don't want to handle logs ourselves in tests
|
||||
// eslint-disable-next-line no-console
|
||||
if (import.meta.env.VITEST) return console[level]
|
||||
|
||||
return (...args: any[]) => {
|
||||
const timestamp = new Date().toUTCString()
|
||||
const { method, file, lineNo } = getFile()
|
||||
|
||||
const ext = file?.split('.').pop() ?? 'default'
|
||||
|
||||
// NOTE: Don't log time and debug in production
|
||||
if (level === 'time' || level === 'debug') {
|
||||
if (import.meta.env.PROD) return
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console[level === 'time' ? 'debug' : level](
|
||||
'%c %c [%s] %c %s %c%s',
|
||||
`background: ${LOG_LEVEL_BACKGROUND[level]};border-radius:1em`,
|
||||
`color: ${TIMESTAMP_COLOR}`,
|
||||
timestamp,
|
||||
`background: ${LOG_LEVEL_BACKGROUND[level]}; color: ${LOG_LEVEL_COLOR[level]}; border-radius: 1em 0 0 1em`,
|
||||
LOG_LEVEL_LABELS[level],
|
||||
`background: ${FILETYPE_BACKGROUND[ext]}; color: ${FILETYPE_COLOR[ext]}; border-radius: 0 1em 1em 0`,
|
||||
method !== undefined
|
||||
? ` ${file}:${lineNo} ${method}() `
|
||||
: ` ${file}:${lineNo} `,
|
||||
...args
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const infoLogger = createLoggerFn('info')
|
||||
const warnLogger = createLoggerFn('warn')
|
||||
const timeLogger = createLoggerFn('time')
|
||||
|
||||
const errorLogger = createLoggerFn('error')
|
||||
const debugLogger = createLoggerFn('debug')
|
||||
|
||||
export const logger = {
|
||||
log: infoLogger,
|
||||
info: infoLogger,
|
||||
warn: warnLogger,
|
||||
error: errorLogger,
|
||||
debug: debugLogger,
|
||||
time: (label: string) => {
|
||||
const now = performance.now()
|
||||
|
||||
timeLogger(`${label}: start`)
|
||||
|
||||
return () => {
|
||||
const duration = performance.now() - now
|
||||
timeLogger(`${label}: ${duration.toFixed(2)}ms`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default () => logger
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import type { MaybeComputedRef } from '@vueuse/core'
|
||||
import type { MaybeRefOrGetter } from '@vueuse/core'
|
||||
|
||||
import { resolveUnref } from '@vueuse/core'
|
||||
import { toValue } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import showdown from 'showdown'
|
||||
|
||||
showdown.extension('openExternalInNewTab', {
|
||||
type: 'output',
|
||||
regex: /<a.+?href.+">/g,
|
||||
regex: /<a.+?href.+?">/g,
|
||||
replace (text: string) {
|
||||
const matches = text.match(/href="(.+)">/) ?? []
|
||||
|
||||
|
|
@ -51,6 +51,6 @@ const markdown = new showdown.Converter({
|
|||
})
|
||||
|
||||
export const useMarkdownRaw = (md: string) => markdown.makeHtml(md)
|
||||
export const useMarkdownComputed = (md: MaybeComputedRef<string>) => computed(() => useMarkdownRaw(resolveUnref(md)))
|
||||
export const useMarkdownComputed = (md: MaybeRefOrGetter<string>) => computed(() => useMarkdownRaw(toValue(md)))
|
||||
|
||||
export default useMarkdownComputed
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export const install: InitModule = ({ store, router }) => {
|
|||
|
||||
const refreshAuth = async (failedRequest: AxiosError) => {
|
||||
if (store.state.auth.oauth.accessToken) {
|
||||
console.log('Failed request, refreshing auth…')
|
||||
logger.warn('Failed request, refreshing auth…')
|
||||
|
||||
try {
|
||||
// maybe the token was expired, let's try to refresh it
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import type { InitModule } from '~/types'
|
|||
import { watchEffect, watch } from 'vue'
|
||||
import { useWebSocket, whenever } from '@vueuse/core'
|
||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import { CLIENT_RADIOS } from '~/utils/clientRadios'
|
||||
|
||||
export const install: InitModule = ({ store }) => {
|
||||
const logger = useLogger()
|
||||
|
||||
watch(() => store.state.instance.instanceUrl, () => {
|
||||
const url = store.getters['instance/absoluteUrl']('/api/v1/activity')
|
||||
.replace(/^http/, 'ws')
|
||||
|
|
@ -25,7 +28,7 @@ export const install: InitModule = ({ store }) => {
|
|||
})
|
||||
|
||||
watchEffect(() => {
|
||||
console.log('Websocket status:', status.value)
|
||||
logger.log('Websocket status:', status.value)
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { InitModule } from '~/types'
|
||||
import type { InitModule, InitModuleContext } from '~/types'
|
||||
|
||||
import store, { key } from '~/store'
|
||||
import router from '~/router'
|
||||
|
|
@ -24,6 +24,7 @@ const app = createApp({
|
|||
data: () => ({ ready: false }),
|
||||
mounted () {
|
||||
this.ready = true
|
||||
logger.info('Everything loaded!')
|
||||
},
|
||||
render () {
|
||||
if (this.ready) {
|
||||
|
|
@ -37,19 +38,35 @@ const app = createApp({
|
|||
app.use(router)
|
||||
app.use(store, key)
|
||||
|
||||
const modules: Array<void | Promise<void>> = []
|
||||
for (const module of Object.values(import.meta.glob('./init/*.ts', { eager: true })) as { install?: InitModule }[]) {
|
||||
modules.push(module.install?.({
|
||||
app,
|
||||
router,
|
||||
store
|
||||
}))
|
||||
const modules: Record<string | 'axios', { install?: InitModule }> = import.meta.glob('./init/*.ts', { eager: true })
|
||||
const moduleContext: InitModuleContext = {
|
||||
app,
|
||||
router,
|
||||
store
|
||||
}
|
||||
|
||||
// Wait for all modules to load
|
||||
Promise.all(modules).finally(() => {
|
||||
app.mount('#app')
|
||||
logger.info('Everything loaded!')
|
||||
})
|
||||
// NOTE: Other modules may depend on network requests and we need to ensure
|
||||
// that all axios interceptors are set before any requests are made
|
||||
// and that the instance url is set before any requests are made
|
||||
const IMPORTANT_MODULES_QUEUE = ['axios', 'instance']
|
||||
const waitForImportantModules = async () => {
|
||||
for (const moduleName of IMPORTANT_MODULES_QUEUE) {
|
||||
const path = `./init/${moduleName}.ts`
|
||||
if (!(path in modules)) {
|
||||
logger.error(`Failed to load important module: ${path}`)
|
||||
continue
|
||||
}
|
||||
|
||||
await modules[path].install?.(moduleContext)
|
||||
delete modules[path]
|
||||
}
|
||||
}
|
||||
|
||||
waitForImportantModules()
|
||||
// NOTE: We load the modules in parallel
|
||||
.then(() => Promise.all(Object.values(modules).map(module => module.install?.(moduleContext))))
|
||||
.catch(error => logger.error('Failed to load modules:', error))
|
||||
// NOTE: We need to mount the app after all modules are loaded
|
||||
.finally(() => app.mount('#app'))
|
||||
|
||||
// TODO (wvffle): Rename filters from useSharedLabels to filters from backend
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import type { NavigationGuardNext, RouteLocationNamedRaw, RouteLocationNormalized } from 'vue-router'
|
||||
import type { Permission } from '~/store/auth'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import store from '~/store'
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
export const hasPermissions = (permission: Permission) => (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
if (store.state.auth.authenticated && store.state.auth.availablePermissions[permission]) {
|
||||
return next()
|
||||
}
|
||||
|
||||
console.log('Not authenticated. Redirecting to library.')
|
||||
logger.warn('Not authenticated. Redirecting to library.')
|
||||
next({ name: 'library.index' })
|
||||
}
|
||||
|
||||
export const requireLoggedIn = (fallbackLocation?: RouteLocationNamedRaw) => (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
if (store.state.auth.authenticated) return next()
|
||||
console.log('!', to)
|
||||
logger.debug('!', to)
|
||||
return next(fallbackLocation ?? { name: 'login', query: { next: to.fullPath } })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ const store: Module<State, RootState> = {
|
|||
try {
|
||||
await axios.post('users/logout')
|
||||
} catch (error) {
|
||||
console.log('Error while logging out, probably logged in via oauth')
|
||||
logger.error('Error while logging out, probably logged in via oauth', error)
|
||||
}
|
||||
|
||||
const modules = [
|
||||
|
|
@ -249,11 +249,11 @@ const store: Module<State, RootState> = {
|
|||
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)
|
||||
logger.log('Redirecting user...', authorizeUrl)
|
||||
window.location.href = authorizeUrl
|
||||
},
|
||||
async handleOauthCallback ({ state, commit, dispatch }, authorizationCode) {
|
||||
console.log('Fetching token...')
|
||||
logger.log('Fetching token...')
|
||||
const payload = {
|
||||
client_id: state.oauth.clientId,
|
||||
client_secret: state.oauth.clientSecret,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type { SUPPORTED_LOCALES } from '~/init/locale'
|
|||
import axios from 'axios'
|
||||
import moment from 'moment'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
type SupportedExtension = 'flac' | 'ogg' | 'mp3' | 'opus' | 'aac' | 'm4a' | 'aiff' | 'aif'
|
||||
|
||||
export type WebSocketEventName = 'inbox.item_added' | 'import.status_updated' | 'mutation.created' | 'mutation.updated'
|
||||
|
|
@ -50,6 +52,8 @@ export interface State {
|
|||
websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers>
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const store: Module<State, RootState> = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
|
|
@ -225,7 +229,9 @@ const store: Module<State, RootState> = {
|
|||
commit('notifications', { type: 'pendingReviewRequests', count: response.data.count })
|
||||
},
|
||||
|
||||
async currentLanguage ({ commit, rootState }, value) {
|
||||
async currentLanguage ({ commit, rootState, state }, value) {
|
||||
if (state.selectedLanguage && state.currentLanguage === value) return
|
||||
|
||||
commit('currentLanguage', value)
|
||||
if (rootState.auth.authenticated) {
|
||||
await axios.post('users/settings', { language: value })
|
||||
|
|
@ -234,7 +240,7 @@ const store: Module<State, RootState> = {
|
|||
|
||||
websocketEvent ({ state }, event: WebSocketEvent) {
|
||||
const handlers = state.websocketEventsHandlers[event.type]
|
||||
console.log('Dispatching websocket event', event, handlers)
|
||||
logger.log('Dispatching websocket event', event, handlers)
|
||||
if (!handlers) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import RadioCard from '~/components/radios/Card.vue'
|
|||
import TagsList from '~/components/tags/List.vue'
|
||||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
type QueryType = 'artists' | 'albums' | 'tracks' | 'playlists' | 'tags' | 'radios' | 'podcasts' | 'series' | 'rss'
|
||||
|
||||
|
|
@ -35,6 +36,8 @@ syncRef(pageQuery, page, {
|
|||
direction: 'both'
|
||||
})
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const q = useRouteQuery('q', '')
|
||||
const query = ref(q.value)
|
||||
syncRef(q, query, { direction: 'ltr' })
|
||||
|
|
@ -209,8 +212,7 @@ const radioConfig = computed(() => {
|
|||
} as RadioConfig
|
||||
}
|
||||
|
||||
// TODO (wvffle): Use logger
|
||||
console.info('This type is not yet supported for radio')
|
||||
logger.warn('This type is not yet supported for radio')
|
||||
}
|
||||
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import useSharedLabels from '~/composables/locale/useSharedLabels'
|
|||
import useOrdering from '~/composables/navigation/useOrdering'
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import usePage from '~/composables/navigation/usePage'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
interface Props extends SmartSearchProps, OrderingProps {
|
||||
mode?: 'card'
|
||||
|
|
@ -37,6 +38,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
orderingConfigName: undefined
|
||||
})
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const search = ref()
|
||||
|
||||
const page = usePage()
|
||||
|
|
@ -68,7 +71,7 @@ const fetchData = async () => {
|
|||
|
||||
result.value = response.data
|
||||
if (query.value === 'resolved:no') {
|
||||
console.log('Refreshing sidebar notifications')
|
||||
logger.log('Refreshing sidebar notifications')
|
||||
store.commit('ui/incrementNotifications', {
|
||||
type: 'pendingReviewReports',
|
||||
value: response.data.count
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import RadioButton from '~/components/radios/Button.vue'
|
|||
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
import useReport from '~/composables/moderation/useReport'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
|
||||
interface Emits {
|
||||
(e: 'followed'): void
|
||||
|
|
@ -31,6 +32,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
displayCopyFid: true
|
||||
})
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const { report, getReportableObjects } = useReport()
|
||||
const store = useStore()
|
||||
|
||||
|
|
@ -80,7 +83,7 @@ const follow = async () => {
|
|||
library.value.follow = response.data
|
||||
emit('followed')
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
logger.error(error)
|
||||
store.commit('ui/addMessage', {
|
||||
content: t('views.content.remote.Card.error.follow', { error }),
|
||||
date: new Date()
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ const fetchData = async () => {
|
|||
playable: true
|
||||
}
|
||||
|
||||
logger.time('Fetching albums')
|
||||
const measureLoading = logger.time('Fetching albums')
|
||||
try {
|
||||
const response = await axios.get('playlists/', {
|
||||
params
|
||||
|
|
@ -76,7 +76,7 @@ const fetchData = async () => {
|
|||
useErrorHandler(error as Error)
|
||||
result.value = undefined
|
||||
} finally {
|
||||
logger.timeEnd('Fetching albums')
|
||||
measureLoading()
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
front/test/specs/composables/useMarkdown.test.ts
Normal file
25
front/test/specs/composables/useMarkdown.test.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { useMarkdownRaw } from '~/composables/useMarkdown'
|
||||
|
||||
describe('useMarkdownRaw', () => {
|
||||
describe('anchors', () => {
|
||||
it('should add target="_blank" to external links', () => {
|
||||
const html = useMarkdownRaw('https://open.audio')
|
||||
expect(html).toBe('<p><a href="https://open.audio" target="_blank" rel="noopener noreferrer">https://open.audio</a></p>')
|
||||
})
|
||||
|
||||
it('should not link raw path', () => {
|
||||
const html = useMarkdownRaw('/library/tags')
|
||||
expect(html).toBe('<p>/library/tags</p>')
|
||||
})
|
||||
|
||||
it('should not add target="_blank" to internal links', () => {
|
||||
const html = useMarkdownRaw('[/library/tags](/library/tags)')
|
||||
expect(html).toBe('<p><a href="/library/tags">/library/tags</a></p>')
|
||||
})
|
||||
|
||||
it('should handle multiple links', () => {
|
||||
const html = useMarkdownRaw('https://open.audio https://funkwhale.audio')
|
||||
expect(html).toBe('<p><a href="https://open.audio" target="_blank" rel="noopener noreferrer">https://open.audio</a> <a href="https://funkwhale.audio" target="_blank" rel="noopener noreferrer">https://funkwhale.audio</a></p>')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -10,7 +10,8 @@
|
|||
"vitest/globals",
|
||||
"vite/client",
|
||||
"vue/ref-macros",
|
||||
"vite-plugin-pwa/client"
|
||||
"vite-plugin-pwa/client",
|
||||
"unplugin-vue-macros/macros-global"
|
||||
],
|
||||
"paths": {
|
||||
"#/*": ["src/worker/*"],
|
||||
|
|
@ -24,5 +25,17 @@
|
|||
"src/**/*.vue",
|
||||
"vite.config.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
],
|
||||
"vueCompilerOptions": {
|
||||
"plugins": [
|
||||
"@vue-macros/volar/define-options",
|
||||
"@vue-macros/volar/define-models",
|
||||
"@vue-macros/volar/define-props",
|
||||
"@vue-macros/volar/define-props-refs",
|
||||
"@vue-macros/volar/short-vmodel",
|
||||
"@vue-macros/volar/define-slots",
|
||||
"@vue-macros/volar/export-props",
|
||||
"@vue-macros/volar/jsx-directive"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import manifest from './pwa-manifest.json'
|
|||
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
|
||||
|
||||
const port = +(process.env.VUE_PORT ?? 8080)
|
||||
|
|
@ -15,8 +16,13 @@ const port = +(process.env.VUE_PORT ?? 8080)
|
|||
export default defineConfig(({ mode }) => ({
|
||||
envPrefix: ['VUE_', 'FUNKWHALE_SENTRY_'],
|
||||
plugins: [
|
||||
// https://github.com/vitejs/vite/tree/main/packages/plugin-vue
|
||||
Vue(),
|
||||
// https://vue-macros.sxzz.moe/
|
||||
VueMacros({
|
||||
plugins: {
|
||||
// https://github.com/vitejs/vite/tree/main/packages/plugin-vue
|
||||
vue: Vue(),
|
||||
}
|
||||
}),
|
||||
|
||||
// https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n
|
||||
VueI18n({
|
||||
|
|
@ -51,6 +57,7 @@ export default defineConfig(({ mode }) => ({
|
|||
}
|
||||
},
|
||||
build: {
|
||||
sourcemap: true,
|
||||
// https://rollupjs.org/configuration-options/
|
||||
rollupOptions: {
|
||||
output: {
|
||||
|
|
@ -79,7 +86,7 @@ export default defineConfig(({ mode }) => ({
|
|||
},
|
||||
setupFiles: [
|
||||
'./test/setup/mock-audio-context.ts',
|
||||
'./test/setup/mock-vue-i18n.ts',
|
||||
'./test/setup/mock-vue-i18n.ts'
|
||||
]
|
||||
}
|
||||
}))
|
||||
|
|
|
|||
2103
front/yarn.lock
2103
front/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue