feat (playbackmanager): add shuffle and repeat features

This commit is contained in:
Fernando Fernández 2021-01-08 02:09:51 +01:00
parent 8b6de3c7a0
commit fc3d8b81e7
5 changed files with 114 additions and 25 deletions

View File

@ -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',

View File

@ -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 {

View File

@ -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 {

View File

@ -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 });

View File

@ -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 });
}
}
};