mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2025-03-04 03:39:25 +00:00
feat (playbackmanager): add shuffle and repeat features
This commit is contained in:
parent
8b6de3c7a0
commit
fc3d8b81e7
@ -53,8 +53,15 @@
|
||||
<v-col cols="6" class="pa-0 d-none d-md-inline">
|
||||
<div class="d-flex flex-column justify-center">
|
||||
<div class="d-flex align-center justify-center">
|
||||
<v-btn disabled icon class="mx-1">
|
||||
<v-icon>mdi-shuffle</v-icon>
|
||||
<v-btn
|
||||
icon
|
||||
:input-value="isShuffling"
|
||||
class="mx-1"
|
||||
@click="toggleShuffle"
|
||||
>
|
||||
<v-icon>{{
|
||||
isShuffling ? 'mdi-shuffle' : 'mdi-shuffle-disabled'
|
||||
}}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon class="mx-1" @click="setPreviousTrack">
|
||||
<v-icon>mdi-skip-previous</v-icon>
|
||||
@ -70,8 +77,8 @@
|
||||
<v-btn icon class="mx-1" @click="setNextTrack">
|
||||
<v-icon>mdi-skip-next</v-icon>
|
||||
</v-btn>
|
||||
<v-btn disabled icon class="mx-1">
|
||||
<v-icon>mdi-repeat-off</v-icon>
|
||||
<v-btn icon class="mx-1" @click="toggleRepeatMode">
|
||||
<v-icon>{{ repeatIcon }}</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<time-slider v-if="!isFullScreenPlayer" />
|
||||
@ -147,6 +154,13 @@ import { PlaybackStatus } from '~/store/playbackManager';
|
||||
export default Vue.extend({
|
||||
mixins: [timeUtils, imageHelper],
|
||||
computed: {
|
||||
footerColor(): string | undefined {
|
||||
if (this.isFullScreenPlayer) {
|
||||
return 'rgba(0,0,0,0.15)';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
isPaused(): boolean {
|
||||
return this.$store.state.playbackManager.status === PlaybackStatus.paused;
|
||||
},
|
||||
@ -155,16 +169,25 @@ export default Vue.extend({
|
||||
this.$store.state.playbackManager.status !== PlaybackStatus.stopped
|
||||
);
|
||||
},
|
||||
isFullScreenPlayer(): boolean {
|
||||
return !this.$store.state.playbackManager.isMinimized;
|
||||
},
|
||||
footerColor(): string | undefined {
|
||||
if (this.isFullScreenPlayer) {
|
||||
return 'rgba(0,0,0,0.15)';
|
||||
repeatIcon(): string {
|
||||
if (
|
||||
this.$store.state.playbackManager.repeatMode === RepeatMode.RepeatAll
|
||||
) {
|
||||
return 'mdi-repeat';
|
||||
} else if (
|
||||
this.$store.state.playbackManager.repeatMode === RepeatMode.RepeatOne
|
||||
) {
|
||||
return 'mdi-repeat-once';
|
||||
} else {
|
||||
return undefined;
|
||||
return 'mdi-repeat-off';
|
||||
}
|
||||
},
|
||||
isShuffling(): boolean {
|
||||
return this.$store.state.playbackManager.isShuffling;
|
||||
},
|
||||
isFullScreenPlayer(): boolean {
|
||||
return this.$route.name === 'playback';
|
||||
},
|
||||
nextTrackName(): string | undefined {
|
||||
const state = this.$store.state.playbackManager;
|
||||
const queue = this.$store.state.playbackManager.queue;
|
||||
@ -189,7 +212,9 @@ export default Vue.extend({
|
||||
'setNextTrack',
|
||||
'setPreviousTrack',
|
||||
'unpause',
|
||||
'pause'
|
||||
'pause',
|
||||
'toggleShuffle',
|
||||
'toggleRepeatMode'
|
||||
]),
|
||||
...mapGetters('playbackManager', [
|
||||
'getCurrentItem',
|
||||
|
@ -19,7 +19,7 @@ import shaka from 'shaka-player/dist/shaka-player.compiled';
|
||||
// @ts-ignore
|
||||
import muxjs from 'mux.js';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { PlaybackInfoResponse } from '@jellyfin/client-axios';
|
||||
import { PlaybackInfoResponse, RepeatMode } from '@jellyfin/client-axios';
|
||||
import { AppState } from '~/store';
|
||||
import timeUtils from '~/mixins/timeUtils';
|
||||
import imageHelper from '~/mixins/imageHelper';
|
||||
@ -102,6 +102,16 @@ export default Vue.extend({
|
||||
(this.$refs.audioPlayer as HTMLAudioElement).volume =
|
||||
this.currentVolume / 100;
|
||||
}
|
||||
break;
|
||||
case 'playbackManager/SET_REPEAT_MODE':
|
||||
if (
|
||||
this.$refs.audioPlayer &&
|
||||
mutation?.payload?.mode === RepeatMode.RepeatOne
|
||||
) {
|
||||
(this.$refs.audioPlayer as HTMLAudioElement).loop = true;
|
||||
} else {
|
||||
(this.$refs.audioPlayer as HTMLAudioElement).loop = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -24,7 +24,11 @@ import shaka from 'shaka-player/dist/shaka-player.compiled';
|
||||
// @ts-ignore
|
||||
import muxjs from 'mux.js';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { ImageType, PlaybackInfoResponse } from '@jellyfin/client-axios';
|
||||
import {
|
||||
ImageType,
|
||||
PlaybackInfoResponse,
|
||||
RepeatMode
|
||||
} from '@jellyfin/client-axios';
|
||||
import { AppState } from '~/store';
|
||||
import timeUtils from '~/mixins/timeUtils';
|
||||
import imageHelper from '~/mixins/imageHelper';
|
||||
@ -108,6 +112,16 @@ export default Vue.extend({
|
||||
(this.$refs.videoPlayer as HTMLVideoElement).currentTime =
|
||||
mutation?.payload?.time;
|
||||
}
|
||||
break;
|
||||
case 'playbackManager/SET_REPEAT_MODE':
|
||||
if (
|
||||
this.$refs.videoPlayer &&
|
||||
mutation?.payload?.mode === RepeatMode.RepeatOne
|
||||
) {
|
||||
(this.$refs.videoPlayer as HTMLVideoElement).loop = true;
|
||||
} else {
|
||||
(this.$refs.videoPlayer as HTMLVideoElement).loop = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -28,7 +28,7 @@ import Vue from 'vue';
|
||||
import { BaseItemDto, ImageType } from '@jellyfin/client-axios';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Swiper, { SwiperOptions } from 'swiper';
|
||||
import { PlaybackStatus, RepeatMode } from '~/store/playbackManager';
|
||||
import { PlaybackStatus } from '~/store/playbackManager';
|
||||
import imageHelper from '~/mixins/imageHelper';
|
||||
|
||||
export default Vue.extend({
|
||||
@ -39,7 +39,6 @@ export default Vue.extend({
|
||||
slidesPerView: 4,
|
||||
centeredSlides: true,
|
||||
initialSlide: 0,
|
||||
loop: true,
|
||||
parallax: true,
|
||||
autoplay: false,
|
||||
effect: 'coverflow',
|
||||
@ -104,9 +103,6 @@ export default Vue.extend({
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.$store.state.playbackManager.repeatMode !== RepeatMode.all) {
|
||||
this.swiperOptions.loop = false;
|
||||
}
|
||||
this.showNavDrawer({ showNavDrawer: false });
|
||||
this.setAppBarOpacity({ opaqueAppBar: false });
|
||||
this.setBackdropOpacity({ value: 0.5 });
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ActionTree, GetterTree, MutationTree } from 'vuex';
|
||||
import { clamp, union } from 'lodash';
|
||||
import { clamp, union, shuffle } from 'lodash';
|
||||
import {
|
||||
BaseItemDto,
|
||||
ChapterInfo,
|
||||
@ -15,9 +15,9 @@ export enum PlaybackStatus {
|
||||
}
|
||||
|
||||
export enum RepeatMode {
|
||||
none,
|
||||
single,
|
||||
all
|
||||
RepeatNone = 'RepeatNone',
|
||||
RepeatOne = 'RepeatOne',
|
||||
RepeatAll = 'RepeatAll'
|
||||
}
|
||||
|
||||
export interface PlaybackManagerState {
|
||||
@ -36,8 +36,9 @@ export interface PlaybackManagerState {
|
||||
isMuted: boolean;
|
||||
isShuffling: boolean;
|
||||
isMinimized: boolean;
|
||||
repeatMode: RepeatMode | null;
|
||||
repeatMode: RepeatMode;
|
||||
queue: BaseItemDto[];
|
||||
originalQueue: BaseItemDto[];
|
||||
playSessionId: string | null;
|
||||
}
|
||||
|
||||
@ -57,8 +58,9 @@ const defaultState = (): PlaybackManagerState => ({
|
||||
isMuted: false,
|
||||
isShuffling: false,
|
||||
isMinimized: true,
|
||||
repeatMode: null,
|
||||
repeatMode: RepeatMode.RepeatNone,
|
||||
queue: [],
|
||||
originalQueue: [],
|
||||
playSessionId: null
|
||||
});
|
||||
|
||||
@ -203,6 +205,31 @@ export const mutations: MutationTree<PlaybackManagerState> = {
|
||||
{ id }: { id: string | null }
|
||||
) {
|
||||
state.playSessionId = id;
|
||||
},
|
||||
SET_REPEAT_MODE(state: PlaybackManagerState, { mode }: { mode: RepeatMode }) {
|
||||
state.repeatMode = mode;
|
||||
},
|
||||
TOGGLE_SHUFFLE(state: PlaybackManagerState) {
|
||||
if (state.queue && state.currentItemIndex !== null) {
|
||||
if (!state.isShuffling) {
|
||||
state.originalQueue = Array.from(state.queue);
|
||||
const item = state.queue[state.currentItemIndex];
|
||||
const itemIndex = state.queue.indexOf(item);
|
||||
state.queue.splice(itemIndex, 1);
|
||||
state.queue = shuffle(state.queue);
|
||||
state.queue.unshift(item);
|
||||
state.currentItemIndex = 0;
|
||||
state.lastItemIndex = null;
|
||||
state.isShuffling = true;
|
||||
} else {
|
||||
const item = state.queue[state.currentItemIndex];
|
||||
state.currentItemIndex = state.originalQueue.indexOf(item);
|
||||
state.queue = Array.from(state.originalQueue);
|
||||
state.originalQueue = [];
|
||||
state.lastItemIndex = null;
|
||||
state.isShuffling = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -241,6 +268,8 @@ export const actions: ActionTree<PlaybackManagerState, PlaybackManagerState> = {
|
||||
state.currentItemIndex + 1 < state.queue.length
|
||||
) {
|
||||
commit('INCREASE_QUEUE_INDEX');
|
||||
} else if (state.repeatMode === RepeatMode.RepeatAll) {
|
||||
commit('SET_CURRENT_ITEM_INDEX', { currentItemIndex: 0 });
|
||||
} else {
|
||||
commit('STOP_PLAYBACK');
|
||||
}
|
||||
@ -286,5 +315,20 @@ export const actions: ActionTree<PlaybackManagerState, PlaybackManagerState> = {
|
||||
},
|
||||
setPlaySessionId({ commit }, { id }) {
|
||||
commit('SET_PLAY_SESSION_ID', { id });
|
||||
},
|
||||
setRepeatMode({ commit }, { mode }) {
|
||||
commit('SET_REPEAT_MODE', { mode });
|
||||
},
|
||||
toggleShuffle({ commit }) {
|
||||
commit('TOGGLE_SHUFFLE');
|
||||
},
|
||||
toggleRepeatMode({ commit, state }) {
|
||||
if (state.repeatMode === RepeatMode.RepeatNone) {
|
||||
commit('SET_REPEAT_MODE', { mode: RepeatMode.RepeatAll });
|
||||
} else if (state.repeatMode === RepeatMode.RepeatAll) {
|
||||
commit('SET_REPEAT_MODE', { mode: RepeatMode.RepeatOne });
|
||||
} else {
|
||||
commit('SET_REPEAT_MODE', { mode: RepeatMode.RepeatNone });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user