feat: improve error handling by using a composable

This commit is contained in:
Phan An 2024-04-23 13:24:29 +02:00
parent 6a0106e352
commit 9fb80a04fc
55 changed files with 299 additions and 278 deletions

View file

@ -30,7 +30,7 @@
<AcceptInvitation v-if="layout === 'invitation'" />
<ResetPasswordForm v-if="layout === 'reset-password'" />
<AppInitializer v-if="initializing" @success="onInitSuccess" @error="onInitError" />
<AppInitializer v-if="authenticated" @success="onInitSuccess" @error="onInitError" />
</template>
<script lang="ts" setup>
@ -79,10 +79,10 @@ const layout = ref<'main' | 'auth' | 'invitation' | 'reset-password'>()
const { isCurrentScreen, getCurrentScreen, resolveRoute } = useRouter()
const online = useOnline()
const initializing = ref(false)
const authenticated = ref(false)
const initialized = ref(false)
const triggerAppInitialization = () => (initializing.value = true)
const triggerAppInitialization = () => (authenticated.value = true)
const onUserLoggedIn = () => {
layout.value = 'main'
@ -90,7 +90,7 @@ const onUserLoggedIn = () => {
}
const onInitSuccess = async () => {
initializing.value = false
authenticated.value = false
initialized.value = true
// call resolveRoute() after init() so that the onResolve hooks can use the stores
@ -99,7 +99,7 @@ const onInitSuccess = async () => {
}
const onInitError = () => {
initializing.value = false
authenticated.value = false
layout.value = 'auth'
}

View file

@ -3,8 +3,9 @@ import { expect, it } from 'vitest'
import { downloadService, playbackService } from '@/services'
import factory from '@/__tests__/factory'
import UnitTestCase from '@/__tests__/UnitTestCase'
import AlbumCard from './AlbumCard.vue'
import { commonStore, songStore } from '@/stores'
import { eventBus } from '@/utils'
import AlbumCard from './AlbumCard.vue'
let album: Album
@ -60,5 +61,13 @@ new class extends UnitTestCase {
expect(fetchMock).toHaveBeenCalledWith(album)
expect(shuffleMock).toHaveBeenCalledWith(songs, true)
})
it('requests context menu', async () => {
this.renderComponent()
const emitMock = this.mock(eventBus, 'emit')
await this.trigger(screen.getByTestId('artist-album-card'), 'contextMenu')
expect(emitMock).toHaveBeenCalledWith('ALBUM_CONTEXT_MENU_REQUESTED', expect.any(MouseEvent), album)
})
}
}

View file

@ -5,6 +5,7 @@ import { downloadService, playbackService } from '@/services'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { commonStore, songStore } from '@/stores'
import ArtistCard from './ArtistCard.vue'
import { eventBus } from '@/utils'
let artist: Artist
@ -62,5 +63,13 @@ new class extends UnitTestCase {
expect(fetchMock).toHaveBeenCalledWith(artist)
expect(playMock).toHaveBeenCalledWith(songs, true)
})
it('requests context menu', async () => {
this.renderComponent()
const emitMock = this.mock(eventBus, 'emit')
await this.trigger(screen.getByTestId('artist-album-card'), 'contextMenu')
expect(emitMock).toHaveBeenCalledWith('ARTIST_CONTEXT_MENU_REQUESTED', expect.any(MouseEvent), artist)
})
}
}

View file

@ -24,14 +24,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import { authService } from '@/services'
import { useMessageToaster } from '@/composables'
import { useErrorHandler, useMessageToaster } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
import FormRow from '@/components/ui/form/FormRow.vue'
const { toastSuccess, toastError } = useMessageToaster()
const emit = defineEmits<{ (e: 'cancel'): void }>()
const email = ref('')
const loading = ref(false)
@ -45,13 +43,9 @@ const requestResetPasswordLink = async () => {
try {
loading.value = true
await authService.requestResetPasswordLink(email.value)
toastSuccess('Password reset link sent. Please check your email.')
} catch (err: any) {
if (err.response.status === 404) {
toastError('No user with this email address found.')
} else {
toastError(err.response.data?.message || 'An unknown error occurred.')
}
useMessageToaster().toastSuccess('Password reset link sent. Please check your email.')
} catch (error: unknown) {
useErrorHandler().handleHttpError(error, { 404: 'No user with this email address found.' })
} finally {
loading.value = false
}

View file

@ -77,8 +77,9 @@ const login = async () => {
password.value = ''
emit('loggedin')
} catch (err) {
} catch (error: unknown) {
failed.value = true
logger.error(error)
window.setTimeout(() => (failed.value = false), 2000)
}
}

View file

@ -21,7 +21,7 @@
import { computed, ref } from 'vue'
import { authService } from '@/services'
import { base64Decode } from '@/utils'
import { useMessageToaster, useRouter } from '@/composables'
import { useErrorHandler, useMessageToaster, useRouter } from '@/composables'
import PasswordField from '@/components/ui/form/PasswordField.vue'
import Btn from '@/components/ui/form/Btn.vue'
@ -38,7 +38,7 @@ const validPayload = computed(() => email.value && token.value)
try {
[email.value, token.value] = base64Decode(decodeURIComponent(getRouteParam('payload')!)).split('|')
} catch (err) {
} catch (error: unknown) {
toastError('Invalid reset password link.')
}
@ -49,8 +49,8 @@ const submit = async () => {
toastSuccess('Password set.')
await authService.login(email.value, password.value)
setTimeout(() => go('/', true))
} catch (err: any) {
toastError(err.response.data?.message || 'Failed to set new password. Please try again.')
} catch (error: unknown) {
useErrorHandler().handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -22,7 +22,7 @@ const loginWithGoogle = async () => {
try {
window.onmessage = (msg: MessageEvent) => emit('success', msg.data)
openPopup('/auth/google/redirect', 'Google Login', 768, 640, window)
} catch (error: any) {
} catch (error: unknown) {
emit('error', error)
}
}

View file

@ -44,16 +44,15 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { invitationService } from '@/services'
import { useDialogBox, useRouter } from '@/composables'
import { parseValidationError } from '@/utils'
import { useErrorHandler, useRouter } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import PasswordField from '@/components/ui/form/PasswordField.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
import FormRow from '@/components/ui/form/FormRow.vue'
const { showErrorDialog } = useDialogBox()
const { getRouteParam } = useRouter()
const { handleHttpError } = useErrorHandler('dialog')
const name = ref('')
const password = ref('')
@ -68,9 +67,8 @@ const submit = async () => {
loading.value = true
await invitationService.accept(token, name.value, password.value)
window.location.href = '/'
} catch (err: any) {
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
showErrorDialog(msg, 'Error')
} catch (error: unknown) {
handleHttpError(error)
} finally {
loading.value = false
}
@ -79,14 +77,8 @@ const submit = async () => {
onMounted(async () => {
try {
userProspect.value = await invitationService.getUserProspect(token)
} catch (err: any) {
if (err.response?.status === 404) {
validToken.value = false
return
}
const msg = err.response?.status === 422 ? parseValidationError(err.response?.data)[0] : 'Unknown error.'
showErrorDialog(msg, 'Error')
} catch (error: unknown) {
handleHttpError(error, { 404: () => (validToken.value = false) })
}
})
</script>

View file

@ -15,13 +15,13 @@
<script setup lang="ts">
import { ref } from 'vue'
import { plusService } from '@/services'
import { forceReloadWindow, logger } from '@/utils'
import { useDialogBox } from '@/composables'
import { forceReloadWindow } from '@/utils'
import { useDialogBox, useErrorHandler } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
const { showSuccessDialog, showErrorDialog } = useDialogBox()
const { showSuccessDialog } = useDialogBox()
const licenseKey = ref('')
const loading = ref(false)
@ -31,9 +31,8 @@ const validateLicenseKey = async () => {
await plusService.activateLicense(licenseKey.value)
await showSuccessDialog('Thanks for purchasing Koel Plus! Koel will now refresh to activate the changes.')
forceReloadWindow()
} catch (e) {
logger.error(e)
await showErrorDialog('Failed to activate Koel Plus. Please try again.')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -72,8 +72,8 @@
import isMobile from 'ismobilejs'
import { defineAsyncComponent, onMounted, ref, watch } from 'vue'
import { albumStore, artistStore, preferenceStore } from '@/stores'
import { useThirdPartyServices } from '@/composables'
import { logger, requireInjection } from '@/utils'
import { useErrorHandler, useThirdPartyServices } from '@/composables'
import { requireInjection } from '@/utils'
import { CurrentSongKey } from '@/symbols'
import ProfileAvatar from '@/components/ui/ProfileAvatar.vue'
@ -103,8 +103,8 @@ const fetchSongInfo = async (_song: Song) => {
try {
artist.value = await artistStore.resolve(_song.artist_id)
album.value = await albumStore.resolve(_song.album_id)
} catch (error) {
logger.log('Failed to fetch media information', error)
} catch (error: unknown) {
useErrorHandler().handleHttpError(error)
}
}

View file

@ -27,7 +27,7 @@
import { ref } from 'vue'
import { playlistFolderStore } from '@/stores'
import { logger } from '@/utils'
import { useDialogBox, useMessageToaster, useOverlay } from '@/composables'
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
@ -35,7 +35,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showErrorDialog, showConfirmDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const name = ref('')
@ -49,8 +49,8 @@ const submit = async () => {
const folder = await playlistFolderStore.store(name.value)
close()
toastSuccess(`Playlist folder "${folder.name}" created.`)
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
logger.error(error)
} finally {
hideOverlay()

View file

@ -35,8 +35,8 @@
<script lang="ts" setup>
import { ref, toRef } from 'vue'
import { playlistFolderStore, playlistStore } from '@/stores'
import { logger, pluralize } from '@/utils'
import { useDialogBox, useMessageToaster, useModal, useOverlay, useRouter } from '@/composables'
import { pluralize } from '@/utils'
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay, useRouter } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
@ -45,7 +45,7 @@ import SelectBox from '@/components/ui/form/SelectBox.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const { go } = useRouter()
const { getFromContext } = useModal()
@ -70,9 +70,8 @@ const submit = async () => {
close()
toastSuccess(`Playlist "${playlist.name}" created.`)
go(`playlist/${playlist.id}`)
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
logger.error(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -19,9 +19,8 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { logger } from '@/utils'
import { playlistFolderStore } from '@/stores'
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
@ -29,7 +28,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const folder = useModal().getFromContext<PlaylistFolder>('folder')
const name = ref(folder.name)
@ -41,9 +40,8 @@ const submit = async () => {
await playlistFolderStore.rename(folder, name.value)
toastSuccess('Playlist folder renamed.')
close()
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
logger.error(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -36,9 +36,8 @@
<script lang="ts" setup>
import { ref, toRef } from 'vue'
import { logger } from '@/utils'
import { playlistFolderStore, playlistStore } from '@/stores'
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
@ -47,7 +46,7 @@ import SelectBox from '@/components/ui/form/SelectBox.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const playlist = useModal().getFromContext<Playlist>('playlist')
const name = ref(playlist.name)
@ -68,9 +67,8 @@ const submit = async () => {
toastSuccess('Playlist updated.')
close()
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
logger.error(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -16,9 +16,9 @@
<script setup lang="ts">
import { sortBy } from 'lodash'
import { computed, onMounted, ref, Ref, toRefs } from 'vue'
import { useAuthorization, useDialogBox } from '@/composables'
import { useAuthorization, useDialogBox, useErrorHandler } from '@/composables'
import { playlistCollaborationService } from '@/services'
import { eventBus, logger } from '@/utils'
import { eventBus } from '@/utils'
import ListSkeleton from '@/components/ui/skeletons/PlaylistCollaboratorListSkeleton.vue'
import ListItem from '@/components/playlist/PlaylistCollaboratorListItem.vue'
@ -62,8 +62,8 @@ const removeCollaborator = async (collaborator: PlaylistCollaborator) => {
collaborators.value = collaborators.value.filter(({ id }) => id !== collaborator.id)
await playlistCollaborationService.removeCollaborator(playlist.value, collaborator)
eventBus.emit('PLAYLIST_COLLABORATOR_REMOVED', playlist.value)
} catch (e) {
logger.error(e)
} catch (error: unknown) {
useErrorHandler().handleHttpError(error)
}
}

View file

@ -54,9 +54,8 @@
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { ref, toRef } from 'vue'
import { playlistFolderStore, playlistStore } from '@/stores'
import { logger } from '@/utils'
import {
useDialogBox,
useDialogBox, useErrorHandler,
useKoelPlus,
useMessageToaster,
useModal,
@ -81,7 +80,7 @@ const {
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const { go } = useRouter()
const { isPlus } = useKoelPlus()
@ -121,9 +120,8 @@ const submit = async () => {
close()
toastSuccess(`Playlist "${playlist.name}" created.`)
go(`playlist/${playlist.id}`)
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
logger.error(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -59,8 +59,16 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { reactive, toRef } from 'vue'
import { cloneDeep, isEqual } from 'lodash'
import { playlistFolderStore, playlistStore } from '@/stores'
import { eventBus, logger } from '@/utils'
import { useDialogBox, useKoelPlus, useMessageToaster, useModal, useOverlay, useSmartPlaylistForm } from '@/composables'
import { eventBus } from '@/utils'
import {
useDialogBox,
useErrorHandler,
useKoelPlus,
useMessageToaster,
useModal,
useOverlay,
useSmartPlaylistForm
} from '@/composables'
import CheckBox from '@/components/ui/form/CheckBox.vue'
import TextInput from '@/components/ui/form/TextInput.vue'
import FormRow from '@/components/ui/form/FormRow.vue'
@ -68,13 +76,11 @@ import SelectBox from '@/components/ui/form/SelectBox.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const {isPlus} = useKoelPlus()
const playlist = useModal().getFromContext<Playlist>('playlist')
const folders = toRef(playlistFolderStore.state, 'folders')
const mutablePlaylist = reactive(cloneDeep(playlist))
const isPristine = () => isEqual(mutablePlaylist.rules, playlist.rules)
@ -112,9 +118,8 @@ const submit = async () => {
toastSuccess(`Playlist "${playlist.name}" updated.`)
eventBus.emit('PLAYLIST_UPDATED', playlist)
close()
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
logger.error(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -73,8 +73,7 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { authService, UpdateCurrentProfileData } from '@/services'
import { logger, parseValidationError } from '@/utils'
import { useDialogBox, useMessageToaster, useAuthorization } from '@/composables'
import { useMessageToaster, useAuthorization, useErrorHandler } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import PasswordField from '@/components/ui/form/PasswordField.vue'
@ -84,13 +83,9 @@ import TextInput from '@/components/ui/form/TextInput.vue'
import FormRow from '@/components/ui/form/FormRow.vue'
const { toastSuccess } = useMessageToaster()
const { showErrorDialog } = useDialogBox()
const profile = ref<UpdateCurrentProfileData>({} as UpdateCurrentProfileData)
const isDemo = window.IS_DEMO
const { currentUser } = useAuthorization()
const profile = ref<UpdateCurrentProfileData>({} as UpdateCurrentProfileData)
const isDemo = window.IS_DEMO
onMounted(() => {
profile.value = {
@ -116,10 +111,8 @@ const update = async () => {
profile.value.current_password = null
delete profile.value.new_password
toastSuccess('Profile updated.')
} catch (error: any) {
const msg = error.response.status === 422 ? parseValidationError(error.response.data)[0] : 'Unknown error.'
await showErrorDialog(msg, 'Error')
logger.log(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
}
}
</script>

View file

@ -37,8 +37,7 @@
import { faCompactDisc } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRef, watch } from 'vue'
import { albumStore, commonStore, preferenceStore as preferences } from '@/stores'
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
import { logger } from '@/utils'
import { useAuthorization, useErrorHandler, useInfiniteScroll, useRouter } from '@/composables'
import AlbumCard from '@/components/album/AlbumCard.vue'
import AlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
@ -54,10 +53,7 @@ const gridContainer = ref<HTMLDivElement>()
const viewMode = ref<ArtistAlbumViewMode>('thumbnails')
const albums = toRef(albumStore.state, 'albums')
const {
ToTopButton,
makeScrollable
} = useInfiniteScroll(gridContainer, async () => await fetchAlbums())
const { ToTopButton, makeScrollable } = useInfiniteScroll(gridContainer, async () => await fetchAlbums())
watch(viewMode, () => (preferences.albums_view_mode = viewMode.value))
@ -87,10 +83,9 @@ useRouter().onScreenActivated('Albums', async () => {
try {
await makeScrollable()
} catch (error) {
logger.error(error)
useMessageToaster().toastError('Failed to load albums.')
} catch (error: unknown) {
initialized = false
useErrorHandler().handleHttpError(error)
}
}
})

View file

@ -90,10 +90,10 @@
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue'
import { eventBus, logger, pluralize } from '@/utils'
import { eventBus, pluralize } from '@/utils'
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
import { downloadService } from '@/services'
import { useDialogBox, useRouter, useSongList, useSongListControls } from '@/composables'
import { useErrorHandler, useRouter, useSongList, useSongListControls } from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import AlbumThumbnail from '@/components/ui/ArtistAlbumThumbnail.vue'
@ -110,7 +110,6 @@ const AlbumInfo = defineAsyncComponent(() => import('@/components/album/AlbumInf
const AlbumCard = defineAsyncComponent(() => import('@/components/album/AlbumCard.vue'))
const AlbumCardSkeleton = defineAsyncComponent(() => import('@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'))
const { showErrorDialog } = useDialogBox()
const { getRouteParam, go, onScreenActivated } = useRouter()
const albumId = ref<number>()
@ -175,9 +174,8 @@ watch(albumId, async id => {
context.entity = album.value
sort('track')
} catch (error) {
logger.error(error)
showErrorDialog('Failed to load album. Please try again.', 'Error')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -63,16 +63,15 @@
<script lang="ts" setup>
import { faVolumeOff } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRef, watch } from 'vue'
import { logger, pluralize, secondsToHumanReadable } from '@/utils'
import { pluralize, secondsToHumanReadable } from '@/utils'
import { commonStore, queueStore, songStore } from '@/stores'
import { playbackService } from '@/services'
import {
useMessageToaster,
useKoelPlus,
useRouter,
useSongList,
useSongListControls,
useLocalStorage
useLocalStorage, useErrorHandler
} from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
@ -101,7 +100,6 @@ const {
const { SongListControls, config } = useSongListControls('Songs')
const { toastError } = useMessageToaster()
const { go, onScreenActivated } = useRouter()
const { isPlus } = useKoelPlus()
const { get: lsGet, set: lsSet } = useLocalStorage()
@ -152,8 +150,7 @@ const fetchSongs = async () => {
own_songs_only: ownSongsOnly.value
})
} catch (error: any) {
toastError(error.response.data?.message || 'Failed to load songs.')
logger.error(error)
useErrorHandler().handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -37,8 +37,7 @@
import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRef, watch } from 'vue'
import { artistStore, commonStore, preferenceStore as preferences } from '@/stores'
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
import { logger } from '@/utils'
import { useAuthorization, useErrorHandler, useInfiniteScroll, useRouter } from '@/composables'
import ArtistCard from '@/components/artist/ArtistCard.vue'
import ArtistCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
@ -86,10 +85,9 @@ useRouter().onScreenActivated('Artists', async () => {
try {
await makeScrollable()
} catch (error: any) {
logger.error(error)
useMessageToaster().toastError(error.response.data?.message || 'Failed to load artists.')
} catch (error: unknown) {
initialized = false
useErrorHandler().handleHttpError(error)
}
}
})

View file

@ -86,10 +86,16 @@
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue'
import { eventBus, logger, pluralize } from '@/utils'
import { eventBus, pluralize } from '@/utils'
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
import { downloadService } from '@/services'
import { useDialogBox, useRouter, useSongList, useSongListControls, useThirdPartyServices } from '@/composables'
import {
useErrorHandler,
useRouter,
useSongList,
useSongListControls,
useThirdPartyServices
} from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import ArtistThumbnail from '@/components/ui/ArtistAlbumThumbnail.vue'
@ -106,7 +112,6 @@ const AlbumCardSkeleton = defineAsyncComponent(() => import('@/components/ui/ske
type Tab = 'Songs' | 'Albums' | 'Info'
const activeTab = ref<Tab>('Songs')
const { showErrorDialog } = useDialogBox()
const { getRouteParam, go, onScreenActivated } = useRouter()
const artistId = ref<number>()
@ -161,9 +166,8 @@ watch(artistId, async id => {
])
context.entity = artist.value
} catch (error) {
logger.error(error)
showErrorDialog('Failed to load artist. Please try again.', 'Error')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -50,10 +50,10 @@
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import { faTags } from '@fortawesome/free-solid-svg-icons'
import { eventBus, logger, pluralize, secondsToHumanReadable } from '@/utils'
import { eventBus, pluralize, secondsToHumanReadable } from '@/utils'
import { playbackService } from '@/services'
import { genreStore, songStore } from '@/stores'
import { useDialogBox, useRouter, useSongList, useSongListControls } from '@/composables'
import { useErrorHandler, useRouter, useSongList, useSongListControls } from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
@ -78,7 +78,6 @@ const {
const { SongListControls, config } = useSongListControls('Genre')
const { showErrorDialog } = useDialogBox()
const { getRouteParam, go, onRouteChanged } = useRouter()
let sortField: SongListSortField = 'title'
@ -122,9 +121,8 @@ const fetch = async () => {
page.value = fetched.nextPage
songs.value.push(...fetched.songs)
} catch (e) {
showErrorDialog('Failed to fetch genre details or genre was not found.', 'Error')
logger.error(e)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -37,9 +37,9 @@
import { faVolumeOff } from '@fortawesome/free-solid-svg-icons'
import { sample } from 'lodash'
import { computed, ref } from 'vue'
import { eventBus, logger } from '@/utils'
import { eventBus } from '@/utils'
import { commonStore, overviewStore, userStore } from '@/stores'
import { useAuthorization, useDialogBox, useRouter } from '@/composables'
import { useAuthorization, useErrorHandler, useRouter } from '@/composables'
import MostPlayedSongs from '@/components/screens/home/MostPlayedSongs.vue'
import RecentlyPlayedSongs from '@/components/screens/home/RecentlyPlayedSongs.vue'
@ -53,7 +53,6 @@ import BtnScrollToTop from '@/components/ui/BtnScrollToTop.vue'
import ScreenBase from '@/components/screens/ScreenBase.vue'
const { isAdmin } = useAuthorization()
const { showErrorDialog } = useDialogBox()
const greetings = [
'Oh hai!',
@ -82,9 +81,8 @@ useRouter().onScreenActivated('Home', async () => {
try {
await overviewStore.init()
initialized = true
} catch (e) {
showErrorDialog('Failed to load home screen data. Please try again.', 'Error')
logger.error(e)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -74,10 +74,17 @@
import { faFile } from '@fortawesome/free-regular-svg-icons'
import { differenceBy } from 'lodash'
import { ref, toRef, watch } from 'vue'
import { eventBus, logger, pluralize } from '@/utils'
import { eventBus, pluralize } from '@/utils'
import { commonStore, playlistStore, songStore } from '@/stores'
import { downloadService, playlistCollaborationService } from '@/services'
import { usePlaylistManagement, useRouter, useSongList, useAuthorization, useSongListControls } from '@/composables'
import {
usePlaylistManagement,
useRouter,
useSongList,
useAuthorization,
useSongListControls,
useErrorHandler
} from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
@ -141,8 +148,8 @@ const fetchDetails = async (refresh = false) => {
sortField.value ??= (playlist.value?.is_smart ? 'title' : 'position')
sort(sortField.value, 'asc')
} catch (e) {
logger.error(e)
} catch (error: unknown) {
useErrorHandler().handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -55,10 +55,10 @@
<script lang="ts" setup>
import { faCoffee } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRef } from 'vue'
import { logger, pluralize } from '@/utils'
import { pluralize } from '@/utils'
import { commonStore, queueStore, songStore } from '@/stores'
import { cache, playbackService } from '@/services'
import { useDialogBox, useRouter, useSongList, useSongListControls } from '@/composables'
import { useErrorHandler, useRouter, useSongList, useSongListControls } from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
@ -66,7 +66,6 @@ import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
import ScreenBase from '@/components/screens/ScreenBase.vue'
const { go, onScreenActivated } = useRouter()
const { showErrorDialog } = useDialogBox()
const {
SongList,
@ -100,9 +99,8 @@ const shuffleSome = async () => {
loading.value = true
await queueStore.fetchRandom()
await playbackService.playFirstInQueue()
} catch (e) {
showErrorDialog('Failed to fetch songs to play. Please try again.', 'Error')
logger.error(e)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
loading.value = false
}
@ -141,9 +139,8 @@ onScreenActivated('Queue', async () => {
if (!song) {
throw new Error('Song not found')
}
} catch (e) {
showErrorDialog('Song not found. Please double check and try again.', 'Error')
logger.error(e)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
return
} finally {
cache.remove('song-to-queue')

View file

@ -41,8 +41,8 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { commonStore, settingStore } from '@/stores'
import { forceReloadWindow, parseValidationError } from '@/utils'
import { useDialogBox, useMessageToaster, useOverlay, useRouter } from '@/composables'
import { forceReloadWindow, } from '@/utils'
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay, useRouter } from '@/composables'
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
import Btn from '@/components/ui/form/Btn.vue'
@ -51,7 +51,7 @@ import ScreenBase from '@/components/screens/ScreenBase.vue'
import FormRow from '@/components/ui/form/FormRow.vue'
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const { go } = useRouter()
const { showOverlay, hideOverlay } = useOverlay()
@ -81,9 +81,8 @@ const save = async () => {
// Make sure we're back to home first.
go('home')
forceReloadWindow()
} catch (err: any) {
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
showErrorDialog(msg, 'Error')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -57,9 +57,9 @@ const render = async (viz: Visualizer) => {
try {
destroyVisualizer = await viz.init(el.value!)
} catch (e) {
} catch (error: unknown) {
// in e.g., DOM testing, the call will fail due to the lack of proper API support
logger.warn('Failed to initialize visualizer', e)
logger.warn('Failed to initialize visualizer', error)
}
}

View file

@ -166,9 +166,9 @@
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { isEqual } from 'lodash'
import { defaultCover, eventBus, logger, pluralize } from '@/utils'
import { defaultCover, eventBus, pluralize } from '@/utils'
import { songStore, SongUpdateData } from '@/stores'
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
import { genres } from '@/config'
import Btn from '@/components/ui/form/Btn.vue'
@ -183,7 +183,7 @@ import TabPanelContainer from '@/components/ui/tabs/TabPanelContainer.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const { getFromContext } = useModal()
const songs = getFromContext<Song[]>('songs')
@ -260,9 +260,8 @@ const submit = async () => {
toastSuccess(`Updated ${pluralize(songs, 'song')}.`)
eventBus.emit('SONGS_UPDATED')
close()
} catch (error) {
showErrorDialog('Something went wrong. Please try again.', 'Error')
logger.error(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -18,7 +18,7 @@ const thumbnailUrl = ref<String | null>(null)
watchEffect(async () => {
try {
thumbnailUrl.value = await albumStore.fetchThumbnail(album.value)
} catch (e) {
} catch (error: unknown) {
thumbnailUrl.value = null
}
})

View file

@ -37,21 +37,19 @@ import { orderBy } from 'lodash'
import { computed, ref, toRefs } from 'vue'
import { albumStore, artistStore, queueStore, songStore } from '@/stores'
import { playbackService } from '@/services'
import { defaultCover, logger } from '@/utils'
import { usePolicies, useMessageToaster, useRouter, useFileReader } from '@/composables'
import { defaultCover } from '@/utils'
import { usePolicies, useMessageToaster, useRouter, useFileReader, useErrorHandler } from '@/composables'
import { acceptedImageTypes } from '@/config'
const { toastSuccess } = useMessageToaster()
const { go } = useRouter()
const { currentUserCan } = usePolicies()
const props = defineProps<{ entity: Album | Artist }>()
const { entity } = toRefs(props)
const droppable = ref(false)
const { toastError } = useMessageToaster()
const { currentUserCan } = usePolicies()
const forAlbum = computed(() => entity.value.type === 'albums')
const sortFields = computed(() => forAlbum.value ? ['disc', 'track'] : ['album_id', 'disc', 'track'])
@ -126,10 +124,7 @@ const onDrop = async (event: DragEvent) => {
await artistStore.uploadImage(entity.value as Artist, url)
}
})
} catch (e: any) {
const message = e.response.data?.message || 'Unknown error.'
toastError(`Failed to upload: ${message}`)
} catch (error: unknown) {
// restore the backup image
if (forAlbum.value) {
(entity.value as Album).cover = backupImage!
@ -137,7 +132,7 @@ const onDrop = async (event: DragEvent) => {
(entity.value as Artist).image = backupImage!
}
logger.error(e)
useErrorHandler().handleHttpError(error)
}
}
</script>

View file

@ -97,8 +97,8 @@ const open = async (t = 0, l = 0) => {
try {
await preventOffScreen(el.value!)
initSubmenus()
} catch (e) {
logger.error(e)
} catch (error: unknown) {
logger.error(error)
// in a non-browser environment (e.g., unit testing), these two functions are broken due to calls to
// getBoundingClientRect() and querySelectorAll()
}

View file

@ -15,21 +15,19 @@
</template>
<script setup lang="ts">
import { computed, ref, toRef, toRefs } from 'vue'
import { defaultCover, logger } from '@/utils'
import { playlistStore, userStore } from '@/stores'
import { useAuthorization, useFileReader, useKoelPlus, useMessageToaster } from '@/composables'
import { computed, ref, toRefs } from 'vue'
import { defaultCover } from '@/utils'
import { playlistStore } from '@/stores'
import { useAuthorization, useErrorHandler, useFileReader, useKoelPlus } from '@/composables'
import { acceptedImageTypes } from '@/config'
const props = defineProps<{ playlist: Playlist }>()
const { playlist } = toRefs(props)
const droppable = ref(false)
const user = toRef(userStore.state, 'current')
const { isAdmin } = useAuthorization()
const { isPlus } = useKoelPlus()
const { toastError } = useMessageToaster()
const backgroundImage = computed(() => `url(${playlist.value.cover || defaultCover })`)
@ -72,14 +70,10 @@ const onDrop = async (event: DragEvent) => {
playlist.value.cover = url
await playlistStore.uploadCover(playlist.value, url)
})
} catch (e: any) {
const message = e.response.data?.message || 'Unknown error.'
toastError(`Failed to upload: ${message}`)
} catch (error: unknown) {
// restore the backup image
playlist.value.cover = backupImage
logger.error(e)
useErrorHandler().handleHttpError(error)
}
}
</script>

View file

@ -16,7 +16,7 @@
<script lang="ts" setup>
import { defineAsyncComponent, ref, toRefs, watch } from 'vue'
import { youTubeService } from '@/services'
import { logger } from '@/utils'
import { useErrorHandler } from '@/composables'
const Btn = defineAsyncComponent(() => import('@/components/ui/form/Btn.vue'))
const YouTubeVideo = defineAsyncComponent(() => import('@/components/ui/youtube/YouTubeVideoItem.vue'))
@ -36,8 +36,8 @@ const loadMore = async () => {
const result = await youTubeService.searchVideosBySong(song.value, nextPageToken)
nextPageToken = result.nextPageToken
videos.value.push(...result.items)
} catch (err) {
logger.error(err)
} catch (error: unknown) {
useErrorHandler().handleHttpError(error)
} finally {
loading.value = false
}

View file

@ -44,8 +44,7 @@
import { isEqual } from 'lodash'
import { reactive } from 'vue'
import { CreateUserData, userStore } from '@/stores'
import { parseValidationError } from '@/utils'
import { useDialogBox, useMessageToaster, useOverlay } from '@/composables'
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TooltipIcon from '@/components/ui/TooltipIcon.vue'
@ -55,7 +54,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showErrorDialog, showConfirmDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const emptyUserData: CreateUserData = {
name: '',
@ -73,9 +72,8 @@ const submit = async () => {
await userStore.store(newUser)
toastSuccess(`New user "${newUser.name}" created.`)
close()
} catch (err: any) {
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
showErrorDialog(msg, 'Error')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -54,9 +54,8 @@
<script lang="ts" setup>
import { isEqual } from 'lodash'
import { reactive, watch } from 'vue'
import { logger, parseValidationError } from '@/utils'
import { UpdateUserData, userStore } from '@/stores'
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
import Btn from '@/components/ui/form/Btn.vue'
import TooltipIcon from '@/components/ui/TooltipIcon.vue'
@ -67,7 +66,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const user = useModal().getFromContext<User>('user')
@ -91,10 +90,8 @@ const submit = async () => {
await userStore.update(user, updateData)
toastSuccess('User profile updated.')
close()
} catch (error: any) {
const msg = error.response.status === 422 ? parseValidationError(error.response.data)[0] : 'Unknown error.'
showErrorDialog(msg, 'Error')
logger.error(error)
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}

View file

@ -7,7 +7,7 @@
<main class="space-y-5">
<FormRow>
<template #label>Emails</template>
<TextArea ref="emailsEl" v-model="rawEmails" name="emails" required title="Emails" />
<TextArea ref="emailsEl" v-model="rawEmails" name="emails" class="!min-h-[8rem]" required title="Emails" />
<template #help>To invite multiple users, input one email per line.</template>
</FormRow>
<FormRow>
@ -28,8 +28,7 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { parseValidationError } from '@/utils'
import { useDialogBox, useMessageToaster, useOverlay } from '@/composables'
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay } from '@/composables'
import { invitationService } from '@/services'
import Btn from '@/components/ui/form/Btn.vue'
@ -40,7 +39,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
const { showOverlay, hideOverlay } = useOverlay()
const { toastSuccess } = useMessageToaster()
const { showErrorDialog, showConfirmDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const emailsEl = ref<InstanceType<typeof TextArea>>()
const rawEmails = ref('')
@ -81,9 +80,8 @@ const submit = async () => {
await invitationService.invite(validEmails, isAdmin.value)
toastSuccess(`Invitation${validEmails.length === 1 ? '' : 's'} sent.`)
close()
} catch (err: any) {
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
showErrorDialog(msg, 'Error')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error)
} finally {
hideOverlay()
}
@ -101,14 +99,3 @@ const maybeClose = async () => {
await showConfirmDialog('Discard all changes?') && close()
}
</script>
<style lang="postcss" scoped>
textarea {
min-height: 8rem !important;
}
small.help {
margin: .75rem 0 .5rem;
display: block;
}
</style>

View file

@ -50,8 +50,8 @@ import { faCircleCheck, faShield } from '@fortawesome/free-solid-svg-icons'
import { computed, toRefs } from 'vue'
import { userStore } from '@/stores'
import { invitationService } from '@/services'
import { useAuthorization, useDialogBox, useMessageToaster, useRouter } from '@/composables'
import { eventBus, parseValidationError } from '@/utils'
import { useAuthorization, useDialogBox, useErrorHandler, useMessageToaster, useRouter } from '@/composables'
import { eventBus } from '@/utils'
import Btn from '@/components/ui/form/Btn.vue'
import UserAvatar from '@/components/user/UserAvatar.vue'
@ -60,7 +60,7 @@ const props = defineProps<{ user: User }>()
const { user } = toRefs(props)
const { toastSuccess } = useMessageToaster()
const { showConfirmDialog, showErrorDialog } = useDialogBox()
const { showConfirmDialog } = useDialogBox()
const { go } = useRouter()
const { currentUser } = useAuthorization()
@ -82,14 +82,10 @@ const revokeInvite = async () => {
try {
await invitationService.revoke(user.value)
toastSuccess(`Invitation for ${user.value.email} revoked.`)
} catch (err: any) {
if (err.response.status === 404) {
showErrorDialog('Cannot revoke the invite. Maybe it has been accepted?', 'Revocation Failed')
return
}
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
showErrorDialog(msg, 'Error')
} catch (error: unknown) {
useErrorHandler('dialog').handleHttpError(error, {
404: 'Cannot revoke the invite. Maybe it has been accepted?'
})
}
}
</script>

View file

@ -4,7 +4,7 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useOverlay } from '@/composables'
import { useErrorHandler, useOverlay } from '@/composables'
import { commonStore, preferenceStore as preferences } from '@/stores'
import { socketListener, socketService, uploadService } from '@/services'
@ -42,9 +42,9 @@ onMounted(async () => {
await socketService.init() && socketListener.listen()
emits('success')
} catch (err) {
emits('error', err)
throw err
} catch (error: unknown) {
useErrorHandler().handleHttpError(error)
emits('error', error)
} finally {
hideOverlay()
}

View file

@ -2,6 +2,7 @@ export * from './useAuthorization'
export * from './useContextMenu'
export * from './useDialogBox'
export * from './useDragAndDrop'
export * from './useErrorHandler'
export * from './useFileReader'
export * from './useFloatingUi'
export * from './useInfiniteScroll'

View file

@ -97,8 +97,8 @@ export const useDroppable = (acceptedTypes: DraggableType[]) => {
try {
return JSON.parse(event.dataTransfer?.getData(`application/x-koel.${type}`)!)
} catch (e) {
logger.warn('Failed to parse dropped data', e)
} catch (error: unknown) {
logger.warn('Failed to parse dropped data', error)
return null
}
}
@ -112,7 +112,7 @@ export const useDroppable = (acceptedTypes: DraggableType[]) => {
default:
return
}
} catch (error) {
} catch (error: unknown) {
logger.error(error, event)
}
}
@ -141,7 +141,7 @@ export const useDroppable = (acceptedTypes: DraggableType[]) => {
default:
throw new Error(`Unknown drag type: ${type}`)
}
} catch (error) {
} catch (error: unknown) {
logger.error(error, event)
return <Song[]>[]
}

View file

@ -0,0 +1,62 @@
import { useMessageToaster, useDialogBox } from '@/composables'
import axios, { AxiosResponse } from 'axios'
import { logger, parseValidationError } from '@/utils'
export interface StatusMessageMap {
[key: AxiosResponse['status']]: string | Closure
}
type ErrorMessageDriver = 'toast' | 'dialog'
export const useErrorHandler = (driver: ErrorMessageDriver = 'toast') => {
const { toastError } = useMessageToaster()
const { showErrorDialog } = useDialogBox()
const showGenericError = () => {
if (driver === 'toast') {
toastError('An unknown error occurred.')
} else {
showErrorDialog('An unknown error occurred.')
}
}
const handleHttpError = (
error: unknown,
statusMessageMap: StatusMessageMap = {}
) => {
logger.error(error)
if (!axios.isAxiosError(error)) {
return showGenericError()
}
if (!error.response?.status || !statusMessageMap.hasOwnProperty(error.response.status)) {
showError('An unknown error occurred.')
return
}
if (error.response?.status === 422) {
return showError(parseValidationError(error.response.data)[0])
}
if (typeof statusMessageMap[error.response!.status!] === 'string') {
showError(statusMessageMap[error.response!.status!]) // @ts-ignore
} else {
return statusMessageMap[error.response!.status!]() // @ts-ignore
}
}
const showError = (message: string) => {
if (driver === 'toast') {
toastError(message)
} else {
showErrorDialog(message)
}
}
return {
handleHttpError,
parseValidationError,
showGenericError
}
}

View file

@ -1,10 +1,10 @@
import { playlistStore } from '@/stores'
import { eventBus, logger, pluralize } from '@/utils'
import { useDialogBox, useMessageToaster } from '@/composables'
import { eventBus, pluralize } from '@/utils'
import { useErrorHandler, useMessageToaster } from '@/composables'
export const usePlaylistManagement = () => {
const { handleHttpError } = useErrorHandler('dialog')
const { toastSuccess } = useMessageToaster()
const { showErrorDialog } = useDialogBox()
const addSongsToPlaylist = async (playlist: Playlist, songs: Song[]) => {
if (playlist.is_smart || songs.length === 0) return
@ -13,9 +13,8 @@ export const usePlaylistManagement = () => {
await playlistStore.addSongs(playlist, songs)
eventBus.emit('PLAYLIST_UPDATED', playlist)
toastSuccess(`Added ${pluralize(songs, 'song')} into "${playlist.name}."`)
} catch (error) {
logger.error(error)
showErrorDialog('Something went wrong. Please try again.', 'Error')
} catch (error: unknown) {
handleHttpError(error)
}
}
@ -26,9 +25,8 @@ export const usePlaylistManagement = () => {
await playlistStore.removeSongs(playlist, songs)
eventBus.emit('PLAYLIST_SONGS_REMOVED', playlist, songs)
toastSuccess(`Removed ${pluralize(songs, 'song')} from "${playlist.name}."`)
} catch (error) {
logger.error(error)
showErrorDialog('Something went wrong. Please try again.', 'Error')
} catch (error: unknown) {
handleHttpError(error)
}
}

View file

@ -95,8 +95,8 @@ export const routes: Route[] = [
const playlist = await playlistCollaborationService.acceptInvite(params.id)
Router.go(`/playlist/${playlist.id}`, true)
return true
} catch (e) {
logger.error(e)
} catch (error: unknown) {
logger.error(error)
return false
}
}

View file

@ -69,8 +69,8 @@ const init = async () => {
state.song = data.song || null
connected.value = true
})
} catch (e) {
logger.error(e)
} catch (error: unknown) {
logger.error(error)
authenticated.value = false
}
}

View file

@ -109,10 +109,10 @@ class PlaybackService {
notification.onclick = () => window.focus()
window.setTimeout(() => notification.close(), 5000)
} catch (e) {
} catch (error: unknown) {
// Notification fails.
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
logger.error(e)
logger.error(error)
}
}
@ -151,7 +151,7 @@ class PlaybackService {
song: song.id,
position: 0
})
} catch (error) {
} catch (error: unknown) {
logger.error(error)
}
@ -161,7 +161,7 @@ class PlaybackService {
await this.player.media.play()
navigator.mediaSession && (navigator.mediaSession.playbackState = 'playing')
this.showNotification(song)
} catch (error) {
} catch (error: unknown) {
// convert this into a warning, as an error will cause Cypress to fail the tests entirely
logger.warn(error)
}
@ -276,7 +276,7 @@ class PlaybackService {
try {
await this.player.media.play()
} catch (error) {
} catch (error: unknown) {
logger.error(error)
}
@ -386,7 +386,7 @@ class PlaybackService {
song: currentSong.id,
position: Math.ceil(media.currentTime)
})
} catch (error) {
} catch (error: unknown) {
logger.error(error)
}
}

View file

@ -3,6 +3,7 @@ import { reactive } from 'vue'
import { http } from '@/services'
import { albumStore, commonStore, overviewStore, songStore } from '@/stores'
import { logger } from '@/utils'
import axios from 'axios'
interface UploadResult {
song: Song
@ -92,10 +93,16 @@ export const uploadService = {
this.proceed() // upload the next file
window.setTimeout(() => this.remove(file), 1000)
} catch (error: any) {
} catch (error: unknown) {
logger.error(error)
file.message = `Upload failed: ${error.response?.data?.message || 'Unknown error'}`
file.status = 'Errored'
if (axios.isAxiosError(error) && error.response?.data?.message) {
file.message = `Upload failed: ${error.response.data.message}`
} else {
file.message = 'Upload failed: Unknown error.'
}
this.proceed() // upload the next file
}
},

View file

@ -71,8 +71,8 @@ export const albumStore = {
album = this.syncWithVault(
await cache.remember<Album>(['album', id], async () => await http.get<Album>(`albums/${id}`))
)[0]
} catch (e) {
logger.error(e)
} catch (error: unknown) {
logger.error(error)
}
}

View file

@ -61,8 +61,8 @@ export const artistStore = {
artist = this.syncWithVault(
await cache.remember<Artist>(['artist', id], async () => await http.get<Artist>(`artists/${id}`))
)[0]
} catch (e) {
logger.error(e)
} catch (error: unknown) {
logger.error(error)
}
}

View file

@ -27,7 +27,7 @@ export const playlistStore = {
try {
this.setupSmartPlaylist(playlist)
this.state.playlists.push(playlist)
} catch (error) {
} catch (error: unknown) {
logger.warn(`Failed to setup smart playlist "${playlist.name}".`, error)
}
}

View file

@ -149,8 +149,8 @@ export const queueStore = {
saveState () {
try {
http.silently.put('queue/state', { songs: this.state.songs.map(({ id }) => id) })
} catch (e) {
logger.error(e)
} catch (error: unknown) {
logger.error(error)
}
}
}

View file

@ -72,8 +72,8 @@ export const songStore = {
if (!song) {
try {
song = this.syncWithVault(await http.get<Song>(`songs/${id}`))[0]
} catch (e) {
logger.error(e)
} catch (error: unknown) {
logger.error(error)
}
}

View file

@ -20,7 +20,7 @@ export const forceReloadWindow = (): void => {
export const copyText = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
} catch (e) {
} catch (error: unknown) {
let copyArea = document.querySelector<HTMLTextAreaElement>('#copyArea')
if (!copyArea) {

View file

@ -51,6 +51,7 @@ export type ServerValidationError = {
message: string
errors: Record<string, string[]>
}
/**
* Parse the validation error from the server into a flattened array of messages.
*/