refactor(up-next): cleanup code

This commit is contained in:
MrTimscampi 2021-07-17 15:44:53 +02:00 committed by Julien Machiels
parent a0ef0783d3
commit f9b04e818c
6 changed files with 173 additions and 307 deletions

View File

@ -1,72 +1,140 @@
<template>
<v-fade-transition>
<v-card class="container white--text pa-4">
<div class="d-flex flex-column flex-grow-1">
<v-card-title class="text-h6 pa-0 my-1 mx-0">
{{ $t('dialog.upNext.nextItemPlayingIn') }}
<span class="primary--text darken-2">
&ensp;{{ timeLeft }} {{ $t('seconds').toLowerCase() }}
</span>
</v-card-title>
<v-card-subtitle
v-if="getCurrentItem.Type === 'Episode'"
class="mt-1 mx-0 mb-2 text-truncate subtitle-1 pa-0"
>
{{ getNextItem.SeriesName }} -
{{
$t('seasonEpisodeAbbrev', {
seasonNumber: getNextItem.ParentIndexNumber,
episodeNumber: getNextItem.IndexNumber
})
}}
<span v-if="$vuetify.breakpoint.smAndUp"> - </span> <br v-else />
{{ getNextItem.Name }}
</v-card-subtitle>
<v-card-subtitle
v-if="getCurrentItem.Type === 'Movie'"
class="mt-1 mx-0 mb-2 text-truncate subtitle-1 pa-0"
>
{{ getNextItem.Name }}
</v-card-subtitle>
<div>
{{ getRuntimeTime(getNextItem.RunTimeTicks) }}
<span class="pl-4"
>{{ $t('endsAt', { time: nextEndsAt(getNextItem.RunTimeTicks) }) }}
</span>
</div>
<v-card-actions>
<v-spacer />
<v-btn class="primary darken-2" @click="$emit('startNext')">
{{ $t('dialog.upNext.startNow') }}
</v-btn>
<v-btn @click="$emit('hide')"> {{ $t('dialog.upNext.hide') }}</v-btn>
</v-card-actions>
</div>
</v-card>
</v-fade-transition>
<v-container
v-if="visible"
class="up-next-dialog pointer-events-none pa-lg-6"
>
<v-row>
<v-col
cols="12"
offset-md="6"
md="6"
offset-lg="8"
lg="4"
offset-xl="9"
xl="3"
>
<v-card class="pointer-events-all">
<v-card-title class="text-h6">
<i18n path="dialog.upNext.nextItemPlayingIn" tag="span">
<template #time>
<span class="primary--text darken-2">
{{ $tc('units.time.seconds', currentItemTimeLeft) }}
</span>
</template>
</i18n>
</v-card-title>
<v-card-subtitle class="text-truncate subtitle-1">
<span v-if="getCurrentItem.Type === 'Episode'">
{{ getNextItem.SeriesName }} -
{{
$t('seasonEpisodeAbbrev', {
seasonNumber: getNextItem.ParentIndexNumber,
episodeNumber: getNextItem.IndexNumber
})
}}
<span v-if="$vuetify.breakpoint.smAndUp"> - </span> <br v-else />
{{ getNextItem.Name }}
</span>
<span v-if="getCurrentItem.Type === 'Movie'">
{{ getNextItem.Name }}
</span>
</v-card-subtitle>
<v-card-text>
<span>
{{ getRuntimeTime(getNextItem.RunTimeTicks) }}
<span class="pl-4"
>{{
$t('endsAt', {
time: getEndsAtTime(getNextItem.RunTimeTicks)
})
}}
</span>
</span>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn class="primary darken-2" depressed @click="setNextTrack()">
{{ $t('dialog.upNext.startNow') }}
</v-btn>
<v-btn depressed outlined @click="isHiddenByUser = true">
{{ $t('dialog.upNext.hide') }}
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { mapGetters } from 'vuex';
import { mapGetters, mapState } from 'vuex';
import timeUtils from '~/mixins/timeUtils';
export default Vue.extend({
mixins: [timeUtils],
props: {
timeLeft: {
type: Number,
required: true
}
data() {
return {
isHiddenByUser: false
};
},
computed: {
...mapGetters('playbackManager', ['getNextItem', 'getCurrentItem'])
},
methods: {
nextEndsAt(runtimeTicks: number): string {
const seconds = this.ticksToMs(runtimeTicks) + this.timeLeft * 1000;
...mapState('playbackManager', [
'currentItemIndex',
'currentTime',
'isMinimized'
]),
...mapGetters('playbackManager', [
'getCurrentlyPlayingMediaType',
'getNextItem',
'getCurrentItem',
'setNextTrack'
]),
currentItemDuration(): number {
return this.ticksToMs(this.getCurrentItem?.RunTimeTicks) / 1000;
},
currentItemTimeLeft(): number {
return Math.round(this.currentItemDuration - this.currentTime);
},
visible(): boolean {
if (
this.isMinimized ||
this.isHiddenByUser ||
this.getCurrentlyPlayingMediaType !== 'Video' ||
!this.getNextItem
) {
return false;
}
return this.$dateFns.format(Date.now() + seconds, 'p');
if (this.currentItemTimeLeft <= this.nextUpDuration) {
return true;
}
return false;
},
nextUpDuration(): number {
// If longer than 5 hours, set the duration to 9 minutes
if (this.currentItemDuration >= 5 * 60 * 60) {
return 540;
}
// If longer than 2 hours, set the duration to 3.5 minutes
else if (this.currentItemDuration >= 2 * 60 * 60) {
return 210;
}
// If longer than 45 minutes, set the duration to 2 minutes
else if (this.currentItemDuration >= 45 * 60) {
return 120;
}
return 45;
}
},
watch: {
currentItemIndex(): void {
this.isHiddenByUser = false;
},
visible(): void {
this.$emit('change', this.visible);
}
}
});
@ -74,22 +142,10 @@ export default Vue.extend({
<style lang="scss" scoped>
@import '~vuetify/src/styles/styles.sass';
.container {
position: fixed;
right: 0;
.up-next-dialog {
position: absolute;
bottom: 0;
width: 100%;
will-change: transform, opacity;
background-color: rgba(map-get($shades, 'black'), 0.7);
user-select: none;
z-index: 6;
-webkit-touch-callout: none;
}
@media #{map-get($display-breakpoints, 'md-and-up')} {
.container {
width: 30em;
margin: 0 2em 10em 0;
}
right: 0;
z-index: 9999;
}
</style>

View File

@ -315,8 +315,8 @@ export default Vue.extend({
if (this.getCurrentAudioTrack) {
if (
(this.sessionInfo?.TranscodingInfo?.AudioCodec &&
this.getCurrentAudioTrack.Codec !==
this.sessionInfo.TranscodingInfo.AudioCodec) ||
this.getCurrentAudioTrack?.Codec !==
this.sessionInfo?.TranscodingInfo?.AudioCodec) ||
!this.sessionInfo?.TranscodingInfo?.IsAudioDirect
) {
return `${this.getCurrentAudioTrack.Codec}${this.sessionInfo?.TranscodingInfo?.AudioCodec}`;
@ -331,15 +331,19 @@ export default Vue.extend({
return this.getCurrentSubtitleTrack?.Codec;
},
mediaAudioChannels(): string | null | undefined {
if (
this.sessionInfo?.TranscodingInfo?.AudioChannels &&
this.getCurrentAudioTrack.Channels !==
this.sessionInfo.TranscodingInfo.AudioChannels
) {
return `${this.getCurrentAudioTrack.Channels}${this.sessionInfo?.TranscodingInfo?.AudioChannels}`;
if (this.getCurrentAudioTrack) {
if (
this.sessionInfo?.TranscodingInfo?.AudioChannels &&
this.getCurrentAudioTrack?.Channels !==
this.sessionInfo?.TranscodingInfo?.AudioChannels
) {
return `${this.getCurrentAudioTrack.Channels}${this.sessionInfo?.TranscodingInfo?.AudioChannels}`;
}
return this.getCurrentAudioTrack?.Channels;
}
return this.getCurrentAudioTrack?.Channels;
return null;
},
mediaTotalBitrate(): string | null | undefined {
if (

View File

@ -23,17 +23,12 @@
v-show="!isMinimized && playbackData"
@close-playback-data="playbackData = false"
/>
<up-next @change="setUpNextVisible" />
<v-hover v-slot="{ hover }">
<v-card class="player-card" width="100%">
<v-container fill-height fluid class="pa-0 justify-center">
<shaka-player ref="videoPlayer" />
</v-container>
<up-next
v-if="showUpNext"
:time-left="getTimeLeft"
@hide="upNextUserHidden = true"
@startNext="setNextTrack()"
/>
<!-- Mini Player Overlay -->
<v-fade-transition>
<v-overlay v-show="hover && isMinimized" absolute>
@ -90,7 +85,9 @@
<!-- Full Screen OSD -->
<v-fade-transition>
<v-overlay
v-show="!isMinimized && showFullScreenOverlay"
v-show="
!isMinimized && showFullScreenOverlay && !isUpNextVisible
"
color="transparent"
absolute
>
@ -267,7 +264,7 @@ export default Vue.extend({
fullScreenVideo: false,
keepOpen: false,
playbackData: false,
upNextUserHidden: false
isUpNextVisible: false
};
},
computed: {
@ -275,52 +272,15 @@ export default Vue.extend({
'getCurrentItem',
'getPreviousItem',
'getNextItem',
'getCurrentlyPlayingMediaType',
'getDuration',
'getTimeLeft'
'getCurrentlyPlayingMediaType'
]),
...mapState('clientSettings', ['darkMode']),
...mapState('playbackManager', ['status', 'isMinimized', 'currentTime']),
isPlaying(): boolean {
return this.status !== PlaybackStatus.Stopped;
},
isPaused(): boolean {
return this.status === PlaybackStatus.Paused;
},
showUpNext(): boolean {
if (
this.isMinimized ||
this.upNextUserHidden ||
this.getCurrentlyPlayingMediaType !== 'Video' ||
!this.getNextItem
) {
return false;
}
// How many seconds left before showing upNext component
// Default 45 seconds for 10-45 minute video
let showUpNextAt = 45;
// 5h
if (this.getDuration >= 5 * 60 * 60) {
// 9 min
showUpNextAt = 540;
}
// 2h
else if (this.getDuration >= 2 * 60 * 60) {
// 3:30 min
showUpNextAt = 210;
}
// 45 min
else if (this.getDuration >= 45 * 60) {
// 2 min
showUpNextAt = 120;
}
if (this.getTimeLeft >= showUpNextAt) {
return false;
}
return true;
}
},
watch: {
@ -346,125 +306,6 @@ export default Vue.extend({
window.addEventListener('keydown', this.handleKeyPress);
}
break;
case 'playbackManager/INCREASE_QUEUE_INDEX':
case 'playbackManager/DECREASE_QUEUE_INDEX':
case 'playbackManager/SET_CURRENT_ITEM_INDEX':
this.upNextUserHidden = false;
// Report playback stop for the previous item
if (
state.playbackManager.currentTime !== null &&
this.getPreviousItem?.Id
) {
this.$api.playState.reportPlaybackStopped(
{
playbackStopInfo: {
ItemId: this.getPreviousItem.Id,
PlaySessionId: state.playbackManager.playSessionId,
PositionTicks: this.msToTicks(
state.playbackManager.currentTime * 1000
)
}
},
{ progress: false }
);
}
// Then report the start of the next one
if (this.getCurrentItem?.Id) {
this.$api.playState.reportPlaybackStart(
{
playbackStartInfo: {
CanSeek: true,
ItemId: this.getCurrentItem.Id,
PlaySessionId: state.playbackManager.playSessionId,
MediaSourceId: state.playbackManager.currentMediaSource?.Id,
AudioStreamIndex:
state.playbackManager.currentAudioStreamIndex,
SubtitleStreamIndex:
state.playbackManager.currentSubtitleStreamIndex
}
},
{ progress: false }
);
this.updateMetadata();
}
this.setLastProgressUpdate({ progress: new Date().getTime() });
break;
case 'playbackManager/SET_CURRENT_TIME': {
if (state.playbackManager.status === PlaybackStatus.Playing) {
const now = new Date().getTime();
if (
this.getCurrentItem !== null &&
now - state.playbackManager.lastProgressUpdate > 1000 &&
state.playbackManager.currentTime !== null
) {
this.$api.playState.reportPlaybackProgress(
{
playbackProgressInfo: {
ItemId: this.getCurrentItem.Id,
PlaySessionId: state.playbackManager.playSessionId,
IsPaused: false,
PositionTicks: Math.round(
this.msToTicks(state.playbackManager.currentTime * 1000)
)
}
},
{ progress: false }
);
this.setLastProgressUpdate({ progress: new Date().getTime() });
}
}
break;
}
case 'playbackManager/STOP_PLAYBACK':
if (state.playbackManager.currentTime !== null) {
this.$api.playState.reportPlaybackStopped(
{
playbackStopInfo: {
ItemId: this.getPreviousItem.Id,
PlaySessionId: state.playbackManager.playSessionId,
PositionTicks: this.msToTicks(
state.playbackManager.currentTime * 1000
)
}
},
{ progress: false }
);
this.setLastProgressUpdate({ progress: 0 });
this.resetMetadata();
this.removeMediaHandlers();
}
break;
case 'playbackManager/PAUSE_PLAYBACK':
if (state.playbackManager.currentTime !== null) {
this.$api.playState.reportPlaybackProgress(
{
playbackProgressInfo: {
ItemId: this.getCurrentItem.Id,
PlaySessionId: state.playbackManager.playSessionId,
IsPaused: true,
PositionTicks: Math.round(
this.msToTicks(state.playbackManager.currentTime * 1000)
)
}
},
{ progress: false }
);
this.setLastProgressUpdate({ progress: new Date().getTime() });
}
break;
}
});
@ -493,6 +334,9 @@ export default Vue.extend({
'skipBackward',
'changeCurrentTime'
]),
setUpNextVisible(isVisible: boolean): void {
this.isUpNextVisible = isVisible;
},
getOsdTimeoutDuration(): number {
// If we're on mobile, the OSD timer must be longer, to account for the lack of pointer movement
if (window.matchMedia('(pointer:fine)').matches) {
@ -546,7 +390,6 @@ export default Vue.extend({
this.setLastItemIndex();
this.resetCurrentItemIndex();
this.setNextTrack();
this.upNextUserHidden = false;
},
handleKeyPress(e: KeyboardEvent): void {
if (!this.isMinimized && this.isPlaying) {

View File

@ -37,7 +37,7 @@
"details": "Details",
"dialog": {
"upNext": {
"nextItemPlayingIn": "Next Item Playing in",
"nextItemPlayingIn": "Starting in {time}",
"startNow": "Start now",
"hide": "Hide"
}
@ -70,6 +70,9 @@
"bitrate": {
"kbps": "{value} kbps",
"mbps": "{value} Mbps"
},
"time": {
"seconds": "{count} second | {count} seconds"
}
},
"fullScreen": "Full screen",
@ -316,7 +319,6 @@
"name": "Search",
"topResults": "Top results"
},
"seconds": "Seconds",
"series": "Series",
"server": "Server",
"serverVersion": "Server version",

View File

@ -111,24 +111,14 @@ const timeUtils = Vue.extend({
* Returns the end time of an item
*
* @param {number} ticks - Ticks of the item to calculate
* @param {boolean} suffix - Whether to add or not the PM or AM prefix
* @returns {string} The resulting string
*/
getEndsAtTime(ticks: number, suffix = true): string {
getEndsAtTime(ticks: number): string {
const ms = this.ticksToMs(ticks);
const endTimeLong = new Date(Date.now() + ms);
let format;
if (!suffix) {
format = endTimeLong.toLocaleString(this.$i18n.locale, {
hour: 'numeric',
minute: 'numeric'
});
} else {
format = this.$dateFns.format(Date.now() + ms, 'p', {
locale: this.$i18n.locale
});
}
const format = this.$dateFns.format(Date.now() + ms, 'p', {
locale: this.$i18n.locale
});
// TODO: Use a Date object
return this.$t('endsAt', {

View File

@ -5,8 +5,8 @@ import {
ChapterInfo,
MediaSourceInfo
} from '@jellyfin/client-axios';
import isNil from 'lodash/isNil';
import { RootState } from '.';
import { ticksToMs } from '~/mixins/timeUtils';
export enum PlaybackStatus {
Stopped,
@ -83,10 +83,7 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
return rootGetters['items/getItems'](state.queue);
},
getCurrentItem: (state, _getters, _rootState, rootGetters) => {
if (
state.currentItemIndex !== null &&
state.queue[state.currentItemIndex]
) {
if (!isNil(state.currentItemIndex) && state.queue[state.currentItemIndex]) {
return rootGetters['items/getItem'](state.queue[state.currentItemIndex]);
}
@ -96,7 +93,7 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
if (state.currentItemIndex === 0) {
return null;
} else if (
state.lastItemIndex !== null &&
!isNil(state.lastItemIndex) &&
state.queue[state.lastItemIndex]
) {
return rootGetters['items/getItem'](state.queue[state.lastItemIndex]);
@ -106,7 +103,7 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
},
getNextItem: (state, _getters, _rootState, rootGetters) => {
if (
state.currentItemIndex !== null &&
!isNil(state.currentItemIndex) &&
state.currentItemIndex + 1 < state.queue.length
) {
return rootGetters['items/getItem'](
@ -119,7 +116,7 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
return null;
},
getCurrentlyPlayingType: (state, _getters, _rootState, rootGetters) => {
if (state.currentItemIndex !== null) {
if (!isNil(state.currentItemIndex)) {
return rootGetters['items/getItem'](state.queue?.[state.currentItemIndex])
?.Type;
}
@ -127,7 +124,7 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
return null;
},
getCurrentlyPlayingMediaType: (state, _getters, _rootState, rootGetters) => {
if (state.currentItemIndex !== null) {
if (!isNil(state.currentItemIndex)) {
return rootGetters['items/getItem'](state.queue?.[state.currentItemIndex])
?.MediaType;
}
@ -135,7 +132,7 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
return null;
},
getCurrentItemSubtitleTracks: (state) => {
if (state.currentMediaSource !== null) {
if (!isNil(state.currentMediaSource)) {
return state.currentMediaSource.MediaStreams?.filter((stream) => {
return stream.Type === 'Subtitle';
});
@ -143,8 +140,8 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
},
getCurrentVideoTrack: (state) => {
if (
state.currentMediaSource !== null &&
state.currentVideoStreamIndex !== undefined
!isNil(state.currentMediaSource) &&
!isNil(state.currentVideoStreamIndex)
) {
return state.currentMediaSource.MediaStreams?.filter((stream) => {
return stream.Type === 'Video';
@ -155,8 +152,8 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
},
getCurrentAudioTrack: (state) => {
if (
state.currentMediaSource !== null &&
state.currentAudioStreamIndex !== undefined
!isNil(state.currentMediaSource) &&
!isNil(state.currentAudioStreamIndex)
) {
return state.currentMediaSource.MediaStreams?.filter((stream) => {
return stream.Type === 'Audio';
@ -167,40 +164,14 @@ export const getters: GetterTree<PlaybackManagerState, RootState> = {
},
getCurrentSubtitleTrack: (state) => {
if (
state.currentMediaSource !== null &&
state.currentSubtitleStreamIndex !== undefined
!isNil(state.currentMediaSource) &&
!isNil(state.currentSubtitleStreamIndex)
) {
return state.currentMediaSource.MediaStreams?.filter((stream) => {
return stream.Type === 'Subtitle';
})[state.currentSubtitleStreamIndex];
}
return null;
},
getCurrentTime: (state) => {
return state.currentTime;
},
getDuration: (state, _getters, _rootState, rootGetters) => {
if (state.currentItemIndex !== null) {
const currentItem = rootGetters['items/getItem'](
state.queue?.[state.currentItemIndex]
);
return ticksToMs(currentItem?.RunTimeTicks) / 1000;
}
return null;
},
getTimeLeft: (state, _getters, _rootState, rootGetters) => {
if (state.currentItemIndex !== null && state.currentTime !== null) {
const currentItem = rootGetters['items/getItem'](
state.queue?.[state.currentItemIndex]
);
const duration = ticksToMs(currentItem?.RunTimeTicks) / 1000;
return parseInt((duration - state.currentTime).toFixed());
}
return null;
}
};