feat: optimize CPU and memory usage
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale/-/merge_requests/2346>
This commit is contained in:
parent
d30d107ef3
commit
a69aeb07e2
|
@ -58,7 +58,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@intlify/eslint-plugin-vue-i18n": "2.0.0",
|
||||
"@intlify/vite-plugin-vue-i18n": "6.0.3",
|
||||
"@intlify/unplugin-vue-i18n": "^0.8.1",
|
||||
"@types/diff": "5.0.2",
|
||||
"@types/dompurify": "2.4.0",
|
||||
"@types/howler": "2.2.7",
|
||||
|
@ -73,6 +73,7 @@
|
|||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vitest/coverage-c8": "0.25.8",
|
||||
"@vue/compiler-sfc": "3.2.45",
|
||||
"@vue/devtools": "^6.5.0",
|
||||
"@vue/eslint-config-standard": "8.0.1",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
"@vue/test-utils": "2.2.7",
|
||||
|
|
|
@ -11,8 +11,8 @@ export interface SoundSource {
|
|||
}
|
||||
|
||||
export interface Sound {
|
||||
preload(): void | Promise<void>
|
||||
dispose(): void
|
||||
preload(): Promise<void>
|
||||
dispose(): Promise<void>
|
||||
|
||||
readonly audioNode: IAudioNode<IAudioContext>
|
||||
readonly isErrored: Ref<boolean>
|
||||
|
@ -23,11 +23,11 @@ export interface Sound {
|
|||
readonly buffered: number
|
||||
looping: boolean
|
||||
|
||||
pause(): void | Promise<void>
|
||||
play(): void | Promise<void>
|
||||
pause(): Promise<void>
|
||||
play(): Promise<void>
|
||||
|
||||
seekTo(seconds: number): void | Promise<void>
|
||||
seekBy(seconds: number): void | Promise<void>
|
||||
seekTo(seconds: number): Promise<void>
|
||||
seekBy(seconds: number): Promise<void>
|
||||
|
||||
onSoundLoop: EventHookOn<Sound>
|
||||
onSoundEnd: EventHookOn<Sound>
|
||||
|
@ -95,19 +95,22 @@ export class HTMLSound implements Sound {
|
|||
this.isLoaded.value = this.#audio.readyState >= 2
|
||||
})
|
||||
|
||||
useEventListener(this.#audio, 'error', () => {
|
||||
useEventListener(this.#audio, 'error', (err) => {
|
||||
console.error('>> AUDIO ERRORED', err, this.__track?.title)
|
||||
this.isErrored.value = true
|
||||
this.isLoaded.value = true
|
||||
})
|
||||
}
|
||||
|
||||
preload () {
|
||||
async preload () {
|
||||
this.isErrored.value = false
|
||||
console.log('CALLING PRELOAD ON', this.__track?.title)
|
||||
this.#audio.load()
|
||||
}
|
||||
|
||||
dispose () {
|
||||
async dispose () {
|
||||
this.audioNode.disconnect()
|
||||
this.#audio.pause()
|
||||
|
||||
// Cancel any request downloading the source
|
||||
this.#audio.src = ''
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import type { QueueItemSource } from '~/types'
|
||||
|
||||
import { whenever, watchDebounced, useCurrentElement, useScrollLock, useFullscreen, useIdle, refAutoReset, useStorage } from '@vueuse/core'
|
||||
import { nextTick, ref, computed, watchEffect, onMounted, defineAsyncComponent } from 'vue'
|
||||
import { nextTick, ref, computed, watchEffect, watch, defineAsyncComponent } from 'vue'
|
||||
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
@ -42,7 +42,7 @@ const {
|
|||
dequeue,
|
||||
playTrack,
|
||||
reorder,
|
||||
endsIn: timeLeft,
|
||||
endsIn,
|
||||
clear
|
||||
} = useQueue()
|
||||
|
||||
|
@ -93,16 +93,6 @@ const scrollToCurrent = (behavior: ScrollBehavior = 'smooth') => {
|
|||
|
||||
watchDebounced(currentTrack, () => scrollToCurrent(), { debounce: 100 })
|
||||
|
||||
const scrollLoop = () => {
|
||||
const visible = [...(list.value?.scroller.$_views.values() ?? [])].map(item => item.nr.index)
|
||||
if (!visible.includes(currentIndex.value)) {
|
||||
list.value?.scrollToIndex(currentIndex.value)
|
||||
requestAnimationFrame(scrollLoop)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(scrollLoop)
|
||||
|
||||
whenever(
|
||||
() => queue.value.length === 0,
|
||||
() => store.commit('ui/queueFocused', null),
|
||||
|
@ -118,6 +108,13 @@ const touchProgress = (event: MouseEvent) => {
|
|||
seekTo(time)
|
||||
}
|
||||
|
||||
const animated = ref(false)
|
||||
watch(currentTrack, async track => {
|
||||
animated.value = false
|
||||
await nextTick()
|
||||
animated.value = true
|
||||
})
|
||||
|
||||
const play = async (index: number) => {
|
||||
isPlaying.value = true
|
||||
return playTrack(index)
|
||||
|
@ -358,8 +355,13 @@ const coverType = useStorage('queue:cover-type', CoverType.COVER_ART)
|
|||
:style="{ 'transform': `translateX(${bufferProgress - 100}%)` }"
|
||||
/>
|
||||
<div
|
||||
class="position bar"
|
||||
:style="{ 'transform': `translateX(calc(${progress}% - 100%)` }"
|
||||
:class="['position bar', { animated }]"
|
||||
:style="{
|
||||
animationDuration: duration + 's',
|
||||
animationPlayState: isPlaying
|
||||
? 'running'
|
||||
: 'paused'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -416,12 +418,11 @@ const coverType = useStorage('queue:cover-type', CoverType.COVER_ART)
|
|||
<div class="sub header">
|
||||
<div>
|
||||
{{ $t('components.Queue.meta.queuePosition', {index: currentIndex +1, length: queue.length}) }}
|
||||
<template v-if="!$store.state.radios.running">
|
||||
<span class="middle ellipses symbol" />
|
||||
<span :title="labels.duration">
|
||||
{{ timeLeft }}
|
||||
</span>
|
||||
</template>
|
||||
<span class="middle pipe symbol" />
|
||||
{{ $t('components.Queue.meta.end') }}
|
||||
<span :title="labels.duration">
|
||||
{{ endsIn }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -434,8 +435,7 @@ const coverType = useStorage('queue:cover-type', CoverType.COVER_ART)
|
|||
:component="QueueItem"
|
||||
:size="50"
|
||||
@reorder="reorderTracks"
|
||||
@visible="scrollToCurrent('auto')"
|
||||
@hidden="scrollLoop"
|
||||
@visible="list.scrollToIndex(currentIndex, 'center')"
|
||||
>
|
||||
<template #default="{ index, item, classlist }">
|
||||
<queue-item
|
||||
|
|
|
@ -150,7 +150,6 @@ const hideArtist = () => {
|
|||
/>
|
||||
<div
|
||||
class="position bar"
|
||||
:style="{ 'transform': `translateX(${progress - 100}%)` }"
|
||||
/>
|
||||
<div
|
||||
class="seek bar"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
|
||||
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||
|
||||
|
@ -23,8 +23,6 @@ interface Props extends PlayOptionsProps {
|
|||
showPosition?: boolean
|
||||
displayActions?: boolean
|
||||
|
||||
hover: boolean
|
||||
|
||||
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
||||
tracks: Track[]
|
||||
isPlayable?: boolean
|
||||
|
@ -57,12 +55,15 @@ const { isPlaying, loading } = usePlayer()
|
|||
const { currentTrack } = useQueue()
|
||||
|
||||
const active = computed(() => props.track.id === currentTrack.value?.id && props.track.position === currentTrack.value?.position)
|
||||
const hover = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[{ active }, 'track-row row']"
|
||||
@dblclick="activateTrack(track, index)"
|
||||
@mousemove="hover = true"
|
||||
@mouseout="hover = false"
|
||||
>
|
||||
<div
|
||||
class="actions one wide left floated column"
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import type { Track } from '~/types'
|
||||
|
||||
import { useElementByPoint, useMouse } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { clone, uniqBy } from 'lodash-es'
|
||||
import { ref, computed } from 'vue'
|
||||
|
@ -71,15 +70,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
unique: true
|
||||
})
|
||||
|
||||
const { x, y } = useMouse({ type: 'client' })
|
||||
const { element } = useElementByPoint({ x, y })
|
||||
const hover = computed(() => {
|
||||
const row = element.value?.closest('.track-row') ?? null
|
||||
return row && allTracks.value.find(track => {
|
||||
return `${track.id}` === row.getAttribute('data-track-id') && `${track.position}` === row.getAttribute('data-track-position')
|
||||
})
|
||||
})
|
||||
|
||||
const currentPage = ref(props.page)
|
||||
const totalTracks = ref(props.total)
|
||||
const fetchDataUrl = ref(props.nextUrl)
|
||||
|
@ -235,8 +225,6 @@ const updatePage = (page: number) => {
|
|||
<track-row
|
||||
v-for="(track, index) in allTracks"
|
||||
:key="`${track.id} ${track.position}`"
|
||||
:data-track-id="track.id"
|
||||
:data-track-position="track.position"
|
||||
:track="track"
|
||||
:index="index"
|
||||
:tracks="allTracks"
|
||||
|
@ -247,7 +235,6 @@ const updatePage = (page: number) => {
|
|||
:display-actions="displayActions"
|
||||
:show-duration="showDuration"
|
||||
:is-podcast="isPodcast"
|
||||
:hover="hover === track"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useMouse, useCurrentElement, useRafFn, useElementByPoint } from '@vueuse/core'
|
||||
import { ref, watchEffect, reactive } from 'vue'
|
||||
import { ref, watchEffect, reactive, watch } from 'vue'
|
||||
|
||||
// @ts-expect-error no typings
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
|
@ -9,7 +9,6 @@ import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
|||
interface Events {
|
||||
(e: 'reorder', from: number, to: number): void
|
||||
(e: 'visible'): void
|
||||
(e: 'hidden'): void
|
||||
}
|
||||
|
||||
interface Props {
|
||||
|
@ -97,7 +96,19 @@ const cleanup = () => {
|
|||
const scrollDirection = ref()
|
||||
const containerSize = reactive({ bottom: 0, top: 0 })
|
||||
const { x, y: screenY } = useMouse({ type: 'client' })
|
||||
const { element: hoveredElement } = useElementByPoint({ x, y: screenY })
|
||||
|
||||
const { element: hoveredElement, pause: dragTrackPause, resume: dragTrackStart } = useElementByPoint({ x, y: screenY })
|
||||
dragTrackPause()
|
||||
|
||||
// Disable element lookup
|
||||
watch(draggedItem, (dragging) => {
|
||||
if (dragging) {
|
||||
dragTrackStart()
|
||||
return
|
||||
}
|
||||
|
||||
dragTrackPause()
|
||||
}, { immediate: true })
|
||||
|
||||
// Find current index and position on both desktop and mobile devices
|
||||
watchEffect(() => {
|
||||
|
@ -138,23 +149,35 @@ const resize = () => {
|
|||
containerSize.bottom = element.offsetHeight + containerSize.top
|
||||
}
|
||||
|
||||
// Scrolling when item held near top/bottom border
|
||||
let lastDate = +new Date()
|
||||
const { resume, pause } = useRafFn(() => {
|
||||
const now = +new Date()
|
||||
const delta = now - lastDate
|
||||
const direction = scrollDirection.value
|
||||
|
||||
if (direction && el.value?.children[0] && !isTouch.value) {
|
||||
el.value.children[0].scrollTop += 200 / delta * (direction === 'up' ? -1 : 1)
|
||||
el.value.children[0].scrollTop += 200 / (now - lastDate) * (direction === 'up' ? -1 : 1)
|
||||
}
|
||||
|
||||
lastDate = now
|
||||
}, { immediate: false })
|
||||
|
||||
const virtualList = ref()
|
||||
const scrollToIndex = (index: number, block: 'center' | 'start' | 'end' = 'start') => {
|
||||
if (!virtualList.value) return
|
||||
|
||||
const position = block === 'start'
|
||||
? index * props.size
|
||||
: block === 'end'
|
||||
? (index + 1) * props.size - virtualList.value.$el.offsetHeight
|
||||
: (index + 0.5) * props.size - virtualList.value.$el.offsetHeight / 2
|
||||
|
||||
virtualList.value.scrollToPosition(position)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
scrollToIndex: (index: number) => virtualList.value?.scrollToItem(index),
|
||||
scroller: virtualList,
|
||||
scrollToIndex,
|
||||
cleanup
|
||||
})
|
||||
</script>
|
||||
|
@ -173,7 +196,6 @@ defineExpose({
|
|||
@touchmove="onTouchmove"
|
||||
@resize="resize"
|
||||
@visible="emit('visible')"
|
||||
@hidden="emit('hidden')"
|
||||
>
|
||||
<template #before>
|
||||
<slot name="header" />
|
||||
|
|
|
@ -168,15 +168,6 @@ export const usePlayer = createGlobalState(() => {
|
|||
|
||||
// Progress
|
||||
const progress = ref(0)
|
||||
useRafFn(() => {
|
||||
const sound = currentSound.value
|
||||
if (!sound) {
|
||||
progress.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
progress.value = sound.currentTime / sound.duration * 100
|
||||
})
|
||||
|
||||
// Loading
|
||||
const loading = computed(() => {
|
||||
|
|
|
@ -296,7 +296,6 @@ export const useQueue = createGlobalState(() => {
|
|||
}
|
||||
|
||||
// Ends in
|
||||
const now = useNow()
|
||||
const endsIn = useTimeAgo(computed(() => {
|
||||
const seconds = sum(
|
||||
queue.value
|
||||
|
@ -304,10 +303,12 @@ export const useQueue = createGlobalState(() => {
|
|||
.map((track) => track.sources[0]?.duration ?? 0)
|
||||
)
|
||||
|
||||
const date = new Date(now.value)
|
||||
const date = new Date()
|
||||
date.setSeconds(date.getSeconds() + seconds)
|
||||
return date
|
||||
}))
|
||||
}), {
|
||||
updateInterval: 0
|
||||
})
|
||||
|
||||
// Clear
|
||||
const clearRadio = ref(false)
|
||||
|
|
|
@ -20,7 +20,7 @@ const AUDIO_ELEMENT = document.createElement('audio')
|
|||
|
||||
const soundPromises = new Map<number, Promise<Sound>>()
|
||||
const soundCache = useLRUCache<number, Sound>({
|
||||
max: 10,
|
||||
max: 3,
|
||||
dispose: (sound) => sound.dispose()
|
||||
})
|
||||
|
||||
|
|
|
@ -192,7 +192,8 @@
|
|||
"queuePosition": "Track {index} of {length}",
|
||||
"startTime": "00:00",
|
||||
"unknownArtist": "Unknown Artist",
|
||||
"unknownAlbum": "Unknown Album"
|
||||
"unknownAlbum": "Unknown Album",
|
||||
"end": "End"
|
||||
}
|
||||
},
|
||||
"RemoteSearchForm": {
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { InitModule } from '~/types'
|
|||
|
||||
import store, { key } from '~/store'
|
||||
import router from '~/router'
|
||||
import devtools from '@vue/devtools'
|
||||
|
||||
import { createApp, defineAsyncComponent, h } from 'vue'
|
||||
|
||||
|
@ -15,6 +16,10 @@ import '~/api'
|
|||
// NOTE: Set the theme as fast as possible
|
||||
useTheme()
|
||||
|
||||
if (import.meta.env.MODE === 'development') {
|
||||
devtools.connect(/* host, port */)
|
||||
}
|
||||
|
||||
const logger = useLogger()
|
||||
logger.info('Loading environment:', import.meta.env.MODE)
|
||||
logger.debug('Environment variables:', import.meta.env)
|
||||
|
@ -34,6 +39,8 @@ const app = createApp({
|
|||
}
|
||||
})
|
||||
|
||||
app.config.performance = false
|
||||
|
||||
app.use(router)
|
||||
app.use(store, key)
|
||||
|
||||
|
|
|
@ -171,6 +171,18 @@
|
|||
.ui.progress:not(.indeterminate)
|
||||
.bar.position:not(.buffer) {
|
||||
background: var(--vibrant-color);
|
||||
will-change: transform;
|
||||
|
||||
|
||||
&.animated {
|
||||
@keyframes progress {
|
||||
0% { transform: translate3d(-100%, 0, 0) }
|
||||
100% { transform: translate3d(0, 0, 0) }
|
||||
}
|
||||
|
||||
animation-name: progress;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
}
|
||||
|
||||
.indicating.progress .bar {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { defineConfig, type PluginOption } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import Inspector from 'vite-plugin-vue-inspector'
|
||||
import VueI18n from '@intlify/vite-plugin-vue-i18n'
|
||||
import VueI18n from '@intlify/unplugin-vue-i18n/vite'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
import { resolve } from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
|
|
1192
front/yarn.lock
1192
front/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue