mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-12-04 20:26:54 +00:00
feat: add music visualiser
This commit is contained in:
parent
6d2157f265
commit
d0b4d5feef
@ -30,6 +30,7 @@
|
||||
"@jellyfin/sdk": "0.8.2",
|
||||
"@vueuse/components": "10.2.1",
|
||||
"@vueuse/core": "10.2.1",
|
||||
"audiomotion-analyzer": "4.0.0",
|
||||
"axios": "1.4.0",
|
||||
"blurhash": "2.0.5",
|
||||
"comlink": "4.4.1",
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-btn
|
||||
:icon="isFavorite ? IMdiHeart : IMdiHeartOutline"
|
||||
size="small"
|
||||
:size="size"
|
||||
:color="isFavorite ? 'primary' : undefined"
|
||||
:loading="loading"
|
||||
@click.stop.prevent="isFavorite = !isFavorite" />
|
||||
@ -15,7 +15,12 @@ import IMdiHeart from 'virtual:icons/mdi/heart';
|
||||
import IMdiHeartOutline from 'virtual:icons/mdi/heart-outline';
|
||||
import { useRemote } from '@/composables';
|
||||
|
||||
const props = defineProps<{ item: BaseItemDto }>();
|
||||
const props = withDefaults(
|
||||
defineProps<{ item: BaseItemDto; size?: string }>(),
|
||||
{
|
||||
size: 'small'
|
||||
}
|
||||
);
|
||||
const remote = useRemote();
|
||||
const loading = ref(false);
|
||||
|
||||
|
44
frontend/src/components/Playback/MusicVisualizer.vue
Normal file
44
frontend/src/components/Playback/MusicVisualizer.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div ref="musicVisualizer" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
||||
import { mediaElementRef } from '@/store';
|
||||
|
||||
let visualizer: AudioMotionAnalyzer;
|
||||
const musicVisualizer = ref(undefined);
|
||||
|
||||
onMounted(() => {
|
||||
visualizer = new AudioMotionAnalyzer(musicVisualizer.value, {
|
||||
source: mediaElementRef.value,
|
||||
mode: 2,
|
||||
gradient: 'prism',
|
||||
reflexRatio: 0.025,
|
||||
overlay: true,
|
||||
showBgColor: true,
|
||||
bgAlpha: 0,
|
||||
fftSize: 16_384,
|
||||
frequencyScale: 'bark',
|
||||
showScaleX: false,
|
||||
smoothing: 0.9
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (visualizer) {
|
||||
visualizer.toggleAnalyzer();
|
||||
visualizer.disconnectInput();
|
||||
visualizer.disconnectOutput();
|
||||
|
||||
if (mediaElementRef.value) {
|
||||
const audioCtx = new AudioContext();
|
||||
|
||||
audioCtx
|
||||
.createMediaElementSource(mediaElementRef.value)
|
||||
.connect(audioCtx.destination);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -10,30 +10,33 @@
|
||||
</app-bar-button-layout>
|
||||
</v-app-bar>
|
||||
<v-col class="px-0">
|
||||
<swiper
|
||||
v-if="playbackManager.queue"
|
||||
class="d-flex justify-center align-center user-select-none"
|
||||
:modules="modules"
|
||||
:slides-per-view="4"
|
||||
centered-slides
|
||||
:autoplay="false"
|
||||
effect="coverflow"
|
||||
:coverflow-effect="coverflowEffect"
|
||||
keyboard
|
||||
a11y
|
||||
virtual
|
||||
@slide-change="onSlideChange"
|
||||
@swiper="setControlledSwiper">
|
||||
<swiper-slide
|
||||
v-for="(item, index) in playbackManager.queue"
|
||||
:key="`${item.Id}-${index}`"
|
||||
:virtual-index="`${item.Id}-${index}`"
|
||||
class="d-flex justify-center">
|
||||
<div class="album-cover">
|
||||
<blurhash-image :item="item" />
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
<transition v-if="playbackManager.queue">
|
||||
<swiper
|
||||
v-if="!isVisualizing"
|
||||
class="d-flex justify-center align-center user-select-none"
|
||||
:modules="modules"
|
||||
:slides-per-view="4"
|
||||
centered-slides
|
||||
:autoplay="false"
|
||||
effect="coverflow"
|
||||
:coverflow-effect="coverflowEffect"
|
||||
keyboard
|
||||
a11y
|
||||
virtual
|
||||
@slide-change="onSlideChange"
|
||||
@swiper="setControlledSwiper">
|
||||
<swiper-slide
|
||||
v-for="(item, index) in playbackManager.queue"
|
||||
:key="`${item.Id}-${index}`"
|
||||
:virtual-index="`${item.Id}-${index}`"
|
||||
class="d-flex justify-center">
|
||||
<div class="album-cover">
|
||||
<blurhash-image :item="item" />
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
<music-visualizer v-else class="visualizer" />
|
||||
</transition>
|
||||
<v-row class="justify-center align-center mt-3">
|
||||
<v-col cols="6">
|
||||
<v-row class="justify-center align-center">
|
||||
@ -50,11 +53,25 @@
|
||||
<time-slider />
|
||||
</v-row>
|
||||
<v-row class="justify-center align-center">
|
||||
<v-btn
|
||||
icon
|
||||
size="x-large"
|
||||
:color="isVisualizing ? 'primary' : undefined"
|
||||
@click.stop.prevent="isVisualizing = !isVisualizing">
|
||||
<v-icon size="x-large">
|
||||
<i-mdi-chart-bar />
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<shuffle-button size="x-large" />
|
||||
<previous-track-button size="x-large" />
|
||||
<play-pause-button size="x-large" />
|
||||
<next-track-button size="x-large" />
|
||||
<repeat-button size="x-large" />
|
||||
<like-button
|
||||
v-if="playbackManager.currentItem"
|
||||
:item="playbackManager?.currentItem"
|
||||
size="x-large"
|
||||
class="active-button" />
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -92,6 +109,7 @@ const modules = [A11y, Keyboard, Virtual, EffectCoverflow];
|
||||
const route = useRoute();
|
||||
|
||||
const playbackManager = playbackManagerStore();
|
||||
|
||||
const coverflowEffect = {
|
||||
depth: 500,
|
||||
slideShadows: false,
|
||||
@ -99,6 +117,8 @@ const coverflowEffect = {
|
||||
stretch: -400
|
||||
};
|
||||
|
||||
const isVisualizing = ref(false);
|
||||
|
||||
const backdropHash = computed(() => {
|
||||
return playbackManager.currentItem
|
||||
? getBlurhash(playbackManager.currentItem, ImageType.Primary)
|
||||
@ -161,4 +181,13 @@ function onSlideChange(): void {
|
||||
min-width: 65vh;
|
||||
width: 65vh;
|
||||
}
|
||||
|
||||
.visualizer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
user-select: none;
|
||||
height: 65vh;
|
||||
padding: 1vh;
|
||||
}
|
||||
</style>
|
||||
|
2
frontend/types/global/components.d.ts
vendored
2
frontend/types/global/components.d.ts
vendored
@ -41,6 +41,7 @@ declare module 'vue' {
|
||||
IMdiBrightnessAuto: typeof import('~icons/mdi/brightness-auto')['default']
|
||||
IMdiCalendarRange: typeof import('~icons/mdi/calendar-range')['default']
|
||||
IMdiCast: typeof import('~icons/mdi/cast')['default']
|
||||
IMdiChartBar: typeof import('~icons/mdi/chart-bar')['default']
|
||||
IMdiCheck: typeof import('~icons/mdi/check')['default']
|
||||
IMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default']
|
||||
IMdiChevronUp: typeof import('~icons/mdi/chevron-up')['default']
|
||||
@ -100,6 +101,7 @@ declare module 'vue' {
|
||||
MediaStreamSelector: typeof import('./../../src/components/Item/MediaStreamSelector.vue')['default']
|
||||
MetadataEditor: typeof import('./../../src/components/Item/Metadata/MetadataEditor.vue')['default']
|
||||
MetadataEditorDialog: typeof import('./../../src/components/Item/Metadata/MetadataEditorDialog.vue')['default']
|
||||
MusicVisualizer: typeof import('./../../src/components/Playback/MusicVisualizer.vue')['default']
|
||||
NavigationDrawer: typeof import('./../../src/components/Layout/Navigation/NavigationDrawer.vue')['default']
|
||||
NextTrackButton: typeof import('./../../src/components/Buttons/Playback/NextTrackButton.vue')['default']
|
||||
PeopleList: typeof import('./../../src/components/Item/PeopleList.vue')['default']
|
||||
|
10
package-lock.json
generated
10
package-lock.json
generated
@ -25,6 +25,7 @@
|
||||
"@jellyfin/sdk": "0.8.2",
|
||||
"@vueuse/components": "10.2.1",
|
||||
"@vueuse/core": "10.2.1",
|
||||
"audiomotion-analyzer": "4.0.0",
|
||||
"axios": "1.4.0",
|
||||
"blurhash": "2.0.5",
|
||||
"comlink": "4.4.1",
|
||||
@ -2068,6 +2069,15 @@
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/audiomotion-analyzer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/audiomotion-analyzer/-/audiomotion-analyzer-4.0.0.tgz",
|
||||
"integrity": "sha512-TTMt+XYFhbm//z2G4gNw5g+eP58GW8E+ON8cv+NmpupOh1kbREgxsat8RLbh5bmQj7sSSB96AyFC9FLe5xrJFA==",
|
||||
"funding": {
|
||||
"type": "Ko-fi",
|
||||
"url": "https://ko-fi.com/hvianna"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.14",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
|
||||
|
Loading…
Reference in New Issue
Block a user