Merge 'master' into 'refactor-img-mixin'

This commit is contained in:
Fernando Fernández 2020-12-15 00:53:41 +01:00
commit dd06b0ee8d
13 changed files with 323 additions and 61 deletions

View File

@ -2,13 +2,12 @@
<v-menu offset-y>
<template #activator="{ on, attrs }">
<div class="d-flex align-center" v-bind="attrs" v-on="on">
<v-avatar size="48" color="primary" class="mr-4">
<v-img
v-if="userImage"
:src="userImage"
:alt="$auth.user.Name"
></v-img>
<v-icon v-else dark>mdi-account</v-icon>
<v-avatar :size="avatarSize" color="primary" class="mr-4">
<v-img :src="userImage" :alt="$auth.user.Name">
<template #placeholder>
<v-icon dark>mdi-account</v-icon>
</template>
</v-img>
</v-avatar>
<h1 class="font-weight-light pb-1">
{{ $auth.user.Name }}
@ -35,6 +34,7 @@ import { mapActions } from 'vuex';
export default Vue.extend({
data() {
return {
avatarSize: 48,
menuItems: [
{
title: this.$t('logout'),
@ -48,13 +48,11 @@ export default Vue.extend({
};
},
computed: {
userImage: {
get() {
if (this.$auth.user?.PrimaryImageTag) {
return `${this.$axios.defaults.baseURL}/Users/${this.$auth.user.Id}/Images/Primary/?tag=${this.$auth.user.PrimaryImageTag}&maxWidth=36`;
} else {
return '';
}
userImage(): string {
if (this.$auth.user?.PrimaryImageTag) {
return `${this.$axios.defaults.baseURL}/Users/${this.$auth.user.Id}/Images/Primary/?tag=${this.$auth.user.PrimaryImageTag}&maxWidth=${this.avatarSize}`;
} else {
return '';
}
}
},

View File

@ -1,7 +1,16 @@
<template>
<v-container fill-height fluid class="pa-0">
<div ref="videoContainer">
<video ref="videoPlayer" :poster="poster" autoplay></video>
<video
ref="videoPlayer"
:poster="poster"
autoplay
@playing="onVideoPlaying"
@timeupdate="onVideoProgress"
@pause="onVideoPause"
@play="onVideoProgress"
@ended="onVideoStopped"
></video>
</div>
</v-container>
</template>
@ -17,6 +26,8 @@ import shaka from 'shaka-player/dist/shaka-player.ui';
import muxjs from 'mux.js';
import { mapActions } from 'vuex';
import 'shaka-player/dist/controls.css';
import { PlaybackInfoResponse } from '@jellyfin/client-axios';
import timeUtils from '~/mixins/timeUtils';
declare global {
interface Window {
@ -26,6 +37,7 @@ declare global {
}
export default Vue.extend({
mixins: [timeUtils],
props: {
item: {
type: Object,
@ -38,6 +50,8 @@ export default Vue.extend({
},
data() {
return {
playbackInfo: {} as PlaybackInfoResponse,
lastProgressUpdate: 0,
source: '',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
player: null as any,
@ -51,27 +65,24 @@ export default Vue.extend({
try {
await this.player.load(newSource);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error code', e.code, 'object', e);
this.pushSnackbarMessage({
message: this.$t('unexpectedError'),
error: 'error'
});
// No need to actually process the error here, the error handler will do this for us
}
}
}
},
async mounted() {
try {
const response = await this.$api.mediaInfo.getPostedPlaybackInfo({
itemId: this.$route.params.itemId,
userId: this.$auth.user.Id,
...this.$playbackProfile
});
this.playbackInfo = (
await this.$api.mediaInfo.getPostedPlaybackInfo({
itemId: this.$route.params.itemId,
userId: this.$auth.user.Id,
playbackInfoDto: { DeviceProfile: this.$playbackProfile }
})
).data;
let mediaSource;
if (response?.data?.MediaSources) {
mediaSource = response.data.MediaSources[0];
if (this.playbackInfo?.MediaSources) {
mediaSource = this.playbackInfo.MediaSources[0];
} else {
throw new Error("This item can't be played.");
}
@ -113,6 +124,8 @@ export default Vue.extend({
this.$refs.videoContainer,
this.$refs.videoPlayer
);
// Register player events
this.player.addEventListener('error', this.onPlayerError);
} else {
this.$nuxt.error({
message: this.$t('browserNotSupported') as string
@ -128,12 +141,99 @@ export default Vue.extend({
beforeDestroy() {
if (this.player) {
window.muxjs = undefined;
this.onVideoStopped(); // Report that the playback is stopping
this.player.removeEventListener('error', this.onPlayerError);
this.player.unload();
this.player.destroy();
}
},
methods: {
...mapActions('snackbar', ['pushSnackbarMessage'])
...mapActions('snackbar', ['pushSnackbarMessage']),
onVideoPlaying(_event: Event) {
// TODO: Move to playback manager
this.$api.playState.reportPlaybackStart(
{
playbackStartInfo: {
CanSeek: true,
ItemId: this.item.Id,
PlaySessionId: this.playbackInfo.PlaySessionId,
MediaSourceId: this.playbackInfo.MediaSources?.[0].Id,
AudioStreamIndex: 0, // TODO: Don't hardcode this
SubtitleStreamIndex: 0 // TODO: Don't hardcode this
}
},
{ progress: false }
);
this.lastProgressUpdate = new Date().getTime();
},
onVideoProgress(_event?: Event) {
// TODO: Move to playback manager
const now = new Date().getTime();
if (now - this.lastProgressUpdate > 1000) {
const currentTime = (this.$refs.videoPlayer as HTMLVideoElement)
.currentTime;
this.$api.playState.reportPlaybackProgress(
{
playbackProgressInfo: {
ItemId: this.item.Id,
PlaySessionId: this.playbackInfo.PlaySessionId,
IsPaused: false,
PositionTicks: Math.round(this.msToTicks(currentTime * 1000))
}
},
{ progress: false }
);
this.lastProgressUpdate = new Date().getTime();
}
},
onVideoPause(_event?: Event) {
// TODO: Move to playback manager
const currentTime = (this.$refs.videoPlayer as HTMLVideoElement)
.currentTime;
this.$api.playState.reportPlaybackProgress(
{
playbackProgressInfo: {
ItemId: this.item.Id,
PlaySessionId: this.playbackInfo.PlaySessionId,
IsPaused: true,
PositionTicks: Math.round(this.msToTicks(currentTime * 1000))
}
},
{ progress: false }
);
},
onVideoStopped(_event?: Event) {
// TODO: Move to playback manager
const currentTime = (this.$refs.videoPlayer as HTMLVideoElement)
.currentTime;
this.$api.playState.reportPlaybackStopped(
{
playbackStopInfo: {
ItemId: this.item.Id,
PlaySessionId: this.playbackInfo.PlaySessionId,
PositionTicks: this.msToTicks(currentTime * 1000)
}
},
{ progress: false }
);
this.lastProgressUpdate = 0;
if (event !== undefined) {
// We're coming from a real end of playback event, so avoid staying on the video screen after playback
// TODO: Once in the playback manager, move this to the end of the queue
this.$router.back();
}
},
onPlayerError(event: Event) {
this.$emit('error', event);
}
}
});
</script>

View File

@ -94,5 +94,15 @@
"numberTracks": "{number} skladeb",
"nextUp": "Další v pořadí",
"latestLibrary": "Nejnovější {libraryName}",
"darkModeToggle": "Přepnout tmavý režim"
"darkModeToggle": "Přepnout tmavý režim",
"errors": {
"messages": {
"videoPlayerError": "Ve video přehrávači došlo k nezvratné chybě.",
"errorCode": "Kód chyby: {errorCode}"
},
"anErrorHappened": "Došlo k chybě"
},
"buttons": {
"ok": "Ok"
}
}

View File

@ -7,6 +7,9 @@
"badRequest": "Bad request. Try again",
"biography": "Biography",
"browserNotSupported": "Your browser is not supported for playing this file.",
"buttons": {
"ok": "Ok"
},
"byArtist": "By",
"changeServer": "Change server",
"changeUser": "Change user",
@ -19,6 +22,13 @@
"dislikes": "Dislikes",
"endDate": "End date",
"endsAt": "Ends at {time}",
"errors": {
"anErrorHappened": "An error happened",
"messages": {
"errorCode": "Error code: {errorCode}",
"videoPlayerError": "The video player encountered an unrecoverable error."
}
},
"episodeNumber": "Episode {episodeNumber}",
"failedToRefreshItems": "Failed to refresh items",
"favorite": "Favorite",
@ -47,9 +57,8 @@
"networks": "Networks",
"nextUp": "Next up",
"noNetworkConnection": "No network connection",
"noResultsFound": "No results found",
"numberTracks": "{number} tracks",
"noResultsFound": "There is nothing here",
"numberTracks": "{number} tracks",
"parentalRatings": "Parental Ratings",
"password": "Password",
"play": "Play",

View File

@ -84,5 +84,15 @@
"required": "Este campo es obligatorio"
},
"shuffleAll": "Mezclar todo",
"libraries": "Bibliotecas"
"libraries": "Bibliotecas",
"errors": {
"messages": {
"errorCode": "Código de error: {errorCode}"
},
"anErrorHappened": "Ocurrió un error"
},
"darkModeToggle": "Cambiar a modo oscuro",
"buttons": {
"ok": "Vale"
}
}

View File

@ -23,7 +23,7 @@
"rating": "Beoordeling",
"releaseDate": "Publicatiedatum",
"serverAddress": "Server adres",
"serverAddressMustBeUrl": "Server adres moet een geldige URL zijn",
"serverAddressMustBeUrl": "Server adres moet een geldig adres zijn",
"serverAddressRequired": "Server adres is verplicht",
"serverNotFound": "Server niet gevonden",
"settings": "Instellingen",
@ -59,7 +59,7 @@
"connect": "Verbinden",
"changeServer": "Verander van server",
"browserNotSupported": "Uw browser kan dit bestandstype niet afspelen.",
"studios": "Studios",
"studios": "Studio's",
"series": "Series",
"selectUser": "Selecteer een gebruiker",
"noResultsFound": "Er is hier niets",
@ -75,5 +75,12 @@
"artists": "Artiesten",
"artist": "Artiest",
"albums": "Albums",
"actors": "Acteurs"
"actors": "Acteurs",
"libraries": "Bibliotheken",
"byArtist": "Door",
"viewDetails": "Bekijk details",
"validation": {
"required": "Dit veld is vereist",
"mustBeUrl": "Dit veld moet een geldige URL zijn"
}
}

View File

@ -9,7 +9,7 @@
"home": "Domov",
"incorrectUsernameOrPassword": "Nesprávne používateľské meno alebo heslo",
"itemNotFound": "Položka nebola nájdená",
"libraryEmpty": "Knižnica je prázdna",
"libraryEmpty": "Táto knižnica je prázdna",
"libraryNotFound": "Knižnica nebola nájdená",
"login": "Prihlásenie",
"logout": "Odhlásenie",
@ -23,7 +23,7 @@
"rating": "Hodnotenie",
"releaseDate": "Dátum vydania",
"serverAddress": "Adresa servera",
"serverAddressMustBeUrl": "Adresa servera musí byť platná URL",
"serverAddressMustBeUrl": "Adresa servera musí byť platná adresa",
"serverAddressRequired": "Adresa servera je požadovaná",
"serverNotFound": "Server nebol nájdený",
"settings": "Nastavenia",
@ -32,28 +32,66 @@
"upNext": "Nasleduje",
"username": "Používateľské meno",
"usernameRequired": "Používateľské meno je požadované",
"youMayAlsoLike": "",
"youMayAlsoLike": "Mohlo by sa vám páčiť",
"years": "Roky",
"videoTypes": "Typy videa",
"parentalRatings": "Rodičovské hodnotenie",
"genres": "Žánre",
"themeVideo": "Úvodné video",
"themeSong": "Úvodná zvučka",
"specialFeatures": "Special Features",
"themeVideo": "Tématické video",
"themeSong": "Tématická hudba",
"specialFeatures": "Bonusový materiál",
"trailer": "Trailer",
"subtitles": "Titulky",
"features": "Features",
"dislikes": "Nepáči sa mi",
"likes": "Páči sa mi",
"favorite": "Obľúbené",
"resumable": "Možné pokračovať",
"resumable": "Pozastaviteľný",
"unplayed": "Neprehrané",
"played": "Prehrané",
"status": "Stav",
"filtersNotFound": "Filtre sa nedajú načitať",
"filtersNotFound": "Filtre sa nedajú načítať",
"unhandledException": "Neošetrená výnimka",
"shows": "Seriály",
"serverVersionTooLow": "Verzia serveru musí byt 10.7.0 nebo novšia",
"noNetworkConnection": "Žiadne pripojenie k sieti",
"browserNotSupported": "Prehrávanie tohto súboru nie je podporované vo vašom prehliadači."
"browserNotSupported": "Prehrávanie tohto súboru nie je podporované vo vašom prehliadači.",
"nextUp": "Nasleduje",
"networks": "Siete",
"name": "Názov",
"moreLikeArtist": "Podobné ako {artist}",
"manualLogin": "Manuálne prihlásenie",
"loginAs": "Prihlásiť sa ako {name}",
"liked": "Páči sa mi to",
"libraries": "Knižnice",
"latestLibrary": "Najnovšie {libraryName}",
"filter": "Filter",
"failedToRefreshItems": "Obnovenie položiek zlyhalo",
"disc": "Disk {discNumber}",
"darkModeToggle": "Prepnúť na tmavý režim",
"connect": "Pripojiť",
"collections": "Kolekcie",
"changeUser": "Zmeniť používateľa",
"changeServer": "Zmeniť server",
"byArtist": "Od",
"biography": "Biografia",
"artists": "Interpreti",
"artist": "Interpret",
"albums": "Albumy",
"actors": "Herci",
"viewDetails": "Zobraziť detaily",
"validation": {
"required": "Toto pole je vyžadované",
"mustBeUrl": "Toto pole musí byť platná URL"
},
"unliked": "Nepáči sa mi to",
"unableGetRelated": "Načítanie súvisiacich položiek nebolo možné",
"studios": "Štúdiá",
"sortByType": "Podľa {type}",
"shuffleAll": "Zamiešať všetko",
"series": "Seriály",
"selectUser": "Vybrať používateľa",
"selectServer": "Vybrať server",
"numberTracks": "{number} stôp",
"noResultsFound": "Nič tu nie je"
}

View File

@ -129,6 +129,7 @@ const imageHelper = Vue.extend({
} else if (maxHeight) {
params.maxHeight = maxHeight;
}
url.search = stringify(params);
return url.toString();

36
mixins/modalHelper.ts Normal file
View File

@ -0,0 +1,36 @@
/**
* Helpers for modals
*
* @mixin
*/
import Vue from 'vue';
declare module '@nuxt/types' {
interface Context {
smallModalWidth: () => string | number;
}
interface NuxtAppOptions {
smallModalWidth: () => string | number;
}
}
declare module 'vue/types/vue' {
interface Vue {
smallModalWidth: () => string | number;
}
}
const modalHelper = Vue.extend({
computed: {
smallModalWidth(): string | number {
if (this.$vuetify.breakpoint.smAndDown) {
return '90%';
} else {
return 600;
}
}
}
});
export default modalHelper;

View File

@ -79,9 +79,9 @@
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsdoc": "^30.7.8",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-prettier": "^3.2.0",
"eslint-plugin-prettier": "^3.3.0",
"eslint-plugin-promise": "^4.2.1",
"husky": "^4.3.5",
"husky": "^4.3.6",
"jest": "^26.6.3",
"jest-canvas-mock": "^2.3.0",
"lint-staged": "^10.5.3",

View File

@ -4,22 +4,52 @@
v-if="item.MediaType === 'Video' || item.MediaType === 'Audio'"
:item="item"
:poster="poster"
@error="handleShakaPlayerError"
/>
<v-dialog v-model="errorDialog" :width="smallModalWidth">
<v-card>
<v-card-title class="headline">
{{ $t('errors.anErrorHappened') }}
</v-card-title>
<v-card-text>
<p>{{ $t('errors.messages.videoPlayerError') }}</p>
<p class="mb-0">
{{ $t('errors.messages.errorCode', { errorCode }) }}
</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="dismissError">
{{ $t('buttons.ok') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { Route } from 'vue-router';
import { BaseItemDto, ImageType } from '@jellyfin/client-axios';
import modalHelper from '~/mixins/modalHelper';
import imageHelper from '~/mixins/imageHelper';
export default Vue.extend({
mixins: [imageHelper],
mixins: [imageHelper, modalHelper],
layout: 'fullpage',
data() {
return {
poster: '',
item: [] as BaseItemDto
errorCode: '',
errorDialog: false,
fromRoute: null as null | Route,
item: [] as BaseItemDto,
poster: ''
};
},
async beforeMount() {
@ -45,6 +75,20 @@ export default Vue.extend({
message: error
});
}
},
methods: {
handleShakaPlayerError(error: any) {
if (error?.detail?.severity === 1) {
// This error is recoverable, ignore for now
} else {
this.errorCode = error?.detail?.code;
this.errorDialog = true;
}
},
dismissError() {
this.errorDialog = false;
this.$router.back();
}
}
});
</script>

View File

@ -6,7 +6,13 @@ import {
} from '@jellyfin/client-axios';
export interface TvShowsState {
/**
* seasons: Stores an array of all seasons
*/
seasons: BaseItemDto[];
/**
* seasonEpisodes: Stores an array for each season containing all the season episodes
*/
seasonEpisodes: BaseItemDto[][];
}
@ -15,17 +21,20 @@ export const state = (): TvShowsState => ({
seasonEpisodes: []
});
type MutationPayload = TvShowsState;
type MutationPayload = {
seasons: BaseItemDto[];
seasonEpisodes: BaseItemDto[];
};
export const mutations: MutationTree<TvShowsState> = {
ADD_TVSHOW_SEASONS(state: TvShowsState, { seasons }: MutationPayload) {
state.seasons.push(...seasons);
state.seasons = seasons;
},
ADD_TVSHOW_SEASON_EPISODES(
state: TvShowsState,
{ seasonEpisodes }: MutationPayload
) {
state.seasonEpisodes.push(...seasonEpisodes);
state.seasonEpisodes.push(seasonEpisodes);
},
CLEAR_TVSHOWS_SEASONS(state: TvShowsState) {
state.seasons = [];

View File

@ -5411,10 +5411,10 @@ eslint-plugin-nuxt@^2.0.0:
semver "^7.3.2"
vue-eslint-parser "^7.1.1"
eslint-plugin-prettier@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.2.0.tgz#af391b2226fa0e15c96f36c733f6e9035dbd952c"
integrity sha512-kOUSJnFjAUFKwVxuzy6sA5yyMx6+o9ino4gCdShzBNx4eyFRudWRYKCFolKjoM40PEiuU6Cn7wBLfq3WsGg7qg==
eslint-plugin-prettier@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz#61e295349a65688ffac0b7808ef0a8244bdd8d40"
integrity sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==
dependencies:
prettier-linter-helpers "^1.0.0"
@ -6810,10 +6810,10 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
husky@^4.3.5:
version "4.3.5"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.5.tgz#ab8d2a0eb6b62fef2853ee3d442c927d89290902"
integrity sha512-E5S/1HMoDDaqsH8kDF5zeKEQbYqe3wL9zJDyqyYqc8I4vHBtAoxkDBGXox0lZ9RI+k5GyB728vZdmnM4bYap+g==
husky@^4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/husky/-/husky-4.3.6.tgz#ebd9dd8b9324aa851f1587318db4cccb7665a13c"
integrity sha512-o6UjVI8xtlWRL5395iWq9LKDyp/9TE7XMOTvIpEVzW638UcGxTmV5cfel6fsk/jbZSTlvfGVJf2svFtybcIZag==
dependencies:
chalk "^4.0.0"
ci-info "^2.0.0"