feat(details): redesign item details

This commit is contained in:
MrTimscampi
2020-10-22 19:31:43 +02:00
parent be1b6a7dc6
commit ec011945ff
36 changed files with 1437 additions and 544 deletions
+5
View File
@@ -2,3 +2,8 @@
//
// The variables you want to modify
// $font-size-root: 20px;
// Globals
$body-font-family: 'Noto Sans',
'Roboto',
sans-serif !default;
+27 -8
View File
@@ -1,6 +1,6 @@
<template>
<nuxt-link :to="itemLink" style="text-decoration: none; color: inherit">
<div class="card-box">
<div class="card-box" :class="{ 'card-margin': !noMargin }">
<div :class="shape || cardType" class="elevation-3">
<div
class="card-content card-content-button d-flex justify-center align-center primary darken-4"
@@ -46,15 +46,18 @@
class="align-self-end"
/>
</div>
<div class="card-overlay d-flex justify-center align-center">
<div
v-if="overlay"
class="card-overlay d-flex justify-center align-center"
>
<v-btn fab color="primary" :to="`/item/${item.Id}/play`">
<v-icon size="36">mdi-play</v-icon>
</v-btn>
</div>
</div>
<div class="card-text">
<div v-if="!noText" class="card-text">
<div class="card-title mt-1">{{ item.Name }}</div>
<div class="card-subtitle grey--text">{{ cardSubtitle }}</div>
<div class="card-subtitle text--secondary">{{ cardSubtitle }}</div>
</div>
</div>
</nuxt-link>
@@ -74,14 +77,30 @@ export default Vue.extend({
},
shape: {
type: [String, Boolean],
required: false,
default: () => {
return false;
}
},
episode: {
type: Boolean,
required: false,
default: () => {
return false;
}
},
overlay: {
type: Boolean,
default: () => {
return true;
}
},
noText: {
type: Boolean,
default: () => {
return false;
}
},
noMargin: {
type: Boolean,
default: () => {
return false;
}
@@ -187,8 +206,9 @@ export default Vue.extend({
.card-box {
cursor: pointer;
padding: 0;
}
.card-margin {
margin: 0.6em;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.portrait-card {
position: relative;
@@ -235,7 +255,6 @@ export default Vue.extend({
right: 1em;
}
.card-overlay {
border-radius: 0.3em;
position: absolute;
background: radial-gradient(
farthest-corner at 50% 50%,
+15 -1
View File
@@ -1,6 +1,6 @@
<template>
<v-col v-show="items.length > 0" class="home-section">
<h1 class="text-h5">
<h1 class="text-h5 font-weight-light header">
<span>{{ section.name }}</span>
</h1>
@@ -132,6 +132,20 @@ h1 {
margin-left: 0.4em;
margin-bottom: 0.25em;
}
.home-section .header span {
padding-left: 0.25em;
}
.home-section .header::before {
background-color: white;
content: '';
position: relative;
display: inline-block;
height: 1px;
bottom: 0.3em;
left: 0;
width: 1.25em;
}
</style>
<style>
+97
View File
@@ -0,0 +1,97 @@
<template>
<div class="text--secondary">
<span v-if="item.ProductionYear && year">{{ item.ProductionYear }}</span>
<span v-if="item.OfficialRating && rating">{{ item.OfficialRating }}</span>
<span v-if="item.CommunityRating && rating"
><v-icon class="rating-icon" size="16">mdi-star</v-icon>
{{ item.CommunityRating }}</span
>
<span v-if="item.RunTimeTicks && runtime">{{ runtimeValue }}</span>
<span v-if="item.RunTimeTicks && endsAt">{{
$t('endsAt', {
time: endsAtValue
})
}}</span>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { formatDuration, intervalToDuration } from 'date-fns';
import { BaseItemDto } from '~/api';
import timeUtils from '~/mixins/timeUtils';
export default Vue.extend({
mixins: [timeUtils],
props: {
item: {
type: Object as () => BaseItemDto,
required: true
},
year: {
type: Boolean,
default: false
},
rating: {
type: Boolean,
default: false
},
runtime: {
type: Boolean,
default: false
},
endsAt: {
type: Boolean,
default: false
}
},
computed: {
runtimeValue: {
get() {
const seconds = this.ticksToMs(this.item.RunTimeTicks);
return formatDuration(intervalToDuration({ start: 0, end: seconds }), {
format: ['hours', 'minutes']
});
}
},
endsAtValue: {
get() {
const seconds = this.ticksToMs(this.item.RunTimeTicks);
return this.$dateFns.format(Date.now() + seconds, 'p');
}
}
},
methods: {
getEndsAtTime(ticks: number): string {
const ms = this.ticksToMs(ticks);
const endTimeLong = new Date(Date.now() + ms);
// TODO: Respect user locale when rendering time
const endTimeShort = endTimeLong.toLocaleString(this.$i18n.locale, {
hour: 'numeric',
minute: 'numeric'
});
// TODO: Use a Date object
return this.$t('endsAt', {
time: endTimeShort
}).toString();
}
}
});
</script>
<style lang="scss" scoped>
span {
margin-left: 0.6em;
margin-right: 0.6em;
}
span:first-of-type {
margin-left: 0;
}
span:last-of-type {
margin-right: 0;
}
.rating-icon {
opacity: 0.6;
}
</style>
+54
View File
@@ -0,0 +1,54 @@
<template>
<v-list color="transparent" two-line>
<div v-if="items.length > 0">
<v-list-item
v-for="item in items"
:key="item.Id"
nuxt
:to="`/person/${item.Id}`"
>
<v-list-item-avatar>
<v-img
v-if="item.PrimaryImageTag"
:src="`${$axios.defaults.baseURL}/Items/${item.Id}/Images/Primary`"
/>
<v-icon class="grey darken-3">mdi-account</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{ item.Name }}</v-list-item-title>
<v-list-item-subtitle>{{
item.Role || item.Type
}}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</div>
<div
v-for="index in skeletonLength"
v-else
:key="index"
class="d-flex align-center mt-5 mb-5"
>
<v-skeleton-loader type="avatar" class="ml-3 mr-3" />
<v-skeleton-loader type="sentences" width="10em" class="pr-5" />
</div>
</v-list>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
items: {
type: Array,
default: () => {
return [];
}
},
skeletonLength: {
type: Number,
default: 3
}
}
});
</script>
+92 -32
View File
@@ -1,36 +1,72 @@
<template>
<v-col v-if="relatedItems" class="related-items">
<h1 class="text-h5">
<span>{{ $t('moreLikeThis') }}</span>
</h1>
<vueper-slides
:bullets="false"
:bullets-outside="false"
:arrows-outside="false"
:visible-slides="8"
:slide-multiple="true"
:breakpoints="breakpoints"
fixed-height="true"
>
<vueper-slide v-for="item in relatedItems" :key="item.Id">
<template v-slot:content>
<card :item="item" />
<div v-if="relatedItems.length > 0">
<div v-if="!vertical" class="related-items">
<h1 class="text-h5 mb-2 ml-2 header">
<span>{{ $t('youMayAlsoLike') }}</span>
</h1>
<vueper-slides
:bullets="false"
:bullets-outside="false"
:arrows-outside="false"
:visible-slides="6"
:slide-multiple="true"
:breakpoints="breakpoints"
fixed-height="true"
>
<vueper-slide v-for="item in relatedItems" :key="item.Id">
<template v-slot:content>
<card :item="item" />
</template>
</vueper-slide>
<template v-slot:arrow-left>
<v-btn icon large>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
</template>
</vueper-slide>
<template v-slot:arrow-left>
<v-btn icon large>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
</template>
<template v-slot:arrow-right>
<v-btn icon large>
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</template>
</vueper-slides>
</v-col>
<template v-slot:arrow-right>
<v-btn icon large>
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</template>
</vueper-slides>
</div>
<div v-if="vertical">
<h2 v-if="relatedItems.length > 0">{{ $t('youMayAlsoLike') }}</h2>
<v-skeleton-loader v-else type="heading" />
<v-list color="transparent" two-line>
<div v-if="relatedItems.length > 0">
<v-list-item
v-for="relatedItem in relatedItems"
:key="relatedItem.Id"
nuxt
:to="`/item/${relatedItem.Id}`"
>
<v-list-item-avatar>
<v-img
:src="`${$axios.defaults.baseURL}/Items/${relatedItem.Id}/Images/Primary`"
/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{ relatedItem.Name }}</v-list-item-title>
<v-list-item-subtitle>{{
relatedItem.ProductionYear
}}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</div>
<div
v-for="index in skeletonLength"
:key="index"
class="d-flex align-center mt-5 mb-5"
>
<v-skeleton-loader type="avatar" class="ml-3 mr-3" />
<v-skeleton-loader type="sentences" width="10em" class="pr-5" />
</div>
</v-list>
</div>
</div>
</template>
<script lang="ts">
@@ -47,6 +83,14 @@ export default Vue.extend({
id: {
type: String,
required: true
},
vertical: {
type: Boolean,
default: false
},
skeletonLength: {
type: Number,
default: 5
}
},
data() {
@@ -67,7 +111,7 @@ export default Vue.extend({
visibleSlides: 6
},
1904: {
visibleSlides: 8
visibleSlides: 6
}
}
};
@@ -81,7 +125,7 @@ export default Vue.extend({
await this.$api.library.getSimilarItems({
itemId: this.id,
userId: this.$auth.user.Id,
limit: 16
limit: this.vertical ? 5 : 12
})
).data.Items as BaseItemDto[];
@@ -93,6 +137,22 @@ export default Vue.extend({
});
</script>
<style lang="scss" scoped>
.header span {
padding-left: 0.25em;
}
.header::before {
background-color: white;
content: '';
position: relative;
display: inline-block;
height: 1px;
bottom: 0.3em;
left: 0;
width: 1.25em;
}
</style>
<style>
.related-items .vueperslides__track {
position: relative;
+30 -29
View File
@@ -1,29 +1,33 @@
<template>
<div>
<v-tabs v-model="seasonTabs" class="mb-3">
<v-tabs v-model="currentTab" class="mb-3" background-color="transparent">
<v-tab v-for="season in seasons" :key="season.Id">
{{ season.Name }}
</v-tab>
</v-tabs>
<v-tabs-items v-model="seasonTabs">
<v-tabs-items v-model="currentTab" class="transparent">
<v-tab-item v-for="season in seasons" :key="season.Id">
<vueper-slides
:visible-slides="3"
:arrows-outside="false"
slide-multiple
:gap="2"
:infinite="false"
:disable-arrows-on-edges="true"
:bullets="false"
:breakpoints="breakpoints"
:dragging-distance="200"
>
<vueper-slide v-for="episode in season.Episodes" :key="episode.Id">
<template v-slot:content>
<card :item="episode" episode />
</template>
</vueper-slide>
</vueper-slides>
<v-list two-line color="transparent">
<v-list-item
v-for="episode in seasonEpisodes[currentTab]"
:key="episode.Id"
nuxt
:to="`/item/${episode.Id}/play`"
>
<v-list-item-avatar tile width="20em" height="12em">
<blurhash-image
v-if="episode.ImageTags && episode.ImageTags.Primary"
:item="episode"
/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{ episode.Name }} </v-list-item-title>
<v-list-item-subtitle>{{
episode.Overview
}}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-tab-item>
</v-tabs-items>
</div>
@@ -33,10 +37,6 @@
import Vue from 'vue';
import { BaseItemDto } from '~/api';
interface Season extends BaseItemDto {
Episodes?: Array<BaseItemDto>;
}
export default Vue.extend({
props: {
item: {
@@ -46,8 +46,9 @@ export default Vue.extend({
},
data() {
return {
seasons: [] as Season[],
seasonTabs: 1,
currentTab: 0,
seasons: [] as BaseItemDto[],
seasonEpisodes: [] as Array<BaseItemDto[]>,
breakpoints: {
600: {
visibleSlides: 2
@@ -74,19 +75,19 @@ export default Vue.extend({
this.seasons = seasons;
// TODO: Lazy load season episodes when clicking on a tab
for (const season of this.seasons) {
const episodes = (
await this.$api.items.getItems({
uId: this.$auth.user.Id,
userId: this.$auth.user.Id,
parentId: season.Id
parentId: season.Id,
fields: 'Overview'
})
).data.Items as BaseItemDto[];
season.Episodes = episodes;
this.seasonEpisodes.push(episodes);
}
this.seasonTabs = 0;
}
});
</script>
+10 -1
View File
@@ -1,7 +1,12 @@
<template>
<div class="skeleton-card">
<v-skeleton-loader type="image" :boilerplate="boilerplate" />
<v-skeleton-loader type="heading" class="mt-1" :boilerplate="boilerplate" />
<v-skeleton-loader
v-if="!noText"
type="heading"
class="mt-1"
:boilerplate="boilerplate"
/>
</div>
</template>
@@ -13,6 +18,10 @@ export default Vue.extend({
boilerplate: {
type: Boolean,
default: false
},
noText: {
type: Boolean,
default: false
}
}
});
+1 -1
View File
@@ -6,7 +6,7 @@
<v-img
v-if="userImage"
:src="userImage"
:alt="$auth.user.name"
:alt="$auth.user.Name"
></v-img>
<v-icon v-else dark>mdi-account</v-icon>
</v-avatar>
+60 -14
View File
@@ -1,7 +1,26 @@
<template>
<v-app ref="app">
<backdrop />
<v-navigation-drawer v-model="drawer" :clipped="clipped" fixed app>
<v-navigation-drawer
v-model="drawer"
:temporary="$vuetify.breakpoint.mobile"
app
>
<template v-slot:prepend>
<div class="d-flex align-center full-width pa-6">
<v-avatar size="48" color="primary" class="mr-4">
<img
v-if="$auth.user.PrimaryImageTag"
:src="`${$axios.defaults.baseURL}/Users/${$auth.user.Id}/Images/Primary/?tag=${$auth.user.PrimaryImageTag}&maxWidth=64`"
/>
<span class="white--text">{{ $auth.user.Name.charAt(0) }}</span>
</v-avatar>
<h1 class="font-weight-light">
{{ $auth.user.Name }}
</h1>
</div>
<v-divider></v-divider>
</template>
<v-list>
<v-list-item
v-for="(item, i) in items"
@@ -54,20 +73,37 @@
</v-list>
</v-navigation-drawer>
<v-app-bar
:clipped-left="clipped"
:hide-on-scroll="$vuetify.breakpoint.mobile"
:color="$vuetify.theme.dark ? '#202020' : undefined"
fixed
:clipped-left="$vuetify.breakpoint.mobile"
class="pl-2 pr-2"
flat
app
>
<v-app-bar-nav-icon @click.stop="drawer = !drawer" />
<v-btn v-if="$route.name !== 'index'" icon @click="$router.back()">
<v-app-bar-nav-icon
v-if="$vuetify.breakpoint.mobile"
@click.stop="drawer = !drawer"
/>
<v-btn
v-if="$route.name !== 'index'"
class="mr-2"
icon
@click="$router.back()"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title v-text="title" />
<v-text-field
class="search-input"
prepend-inner-icon="mdi-magnify"
placeholder="Search"
max-width="15em"
dense
outlined
filled
flat
hide-details
single-line
/>
<v-spacer />
<locale-switcher />
<locale-switcher class="mr-2" />
<user-button v-if="$auth.loggedIn" />
</v-app-bar>
<v-main>
@@ -83,15 +119,11 @@ import Vue from 'vue';
export default Vue.extend({
data() {
return {
clipped: true,
drawer: true,
miniVariant: false
opacity: 0
};
},
computed: {
title() {
return this.$store.state.page.title;
},
items() {
return [
{
@@ -124,4 +156,18 @@ export default Vue.extend({
.v-application {
background-color: var(--v-background-base) !important;
}
.v-app-bar:not(.v-app-bar--is-scrolled) {
background-color: transparent !important;
}
.v-app-bar .v-app-bar--is-scrolled {
padding-top: 0;
padding-bottom: 0;
background-color: rgba(32, 32, 32, 1) !important;
}
.search-input {
max-width: 15em;
}
</style>
+34 -32
View File
@@ -1,34 +1,36 @@
{
"badRequest": "Špatný požadavek. Zkuste to znovu",
"continueListening": "Pokračovat v poslechu",
"continueWatching": "Pokračovat ve sledování",
"episodeNumber": "Epizoda {episodeNumber}",
"home": "Domů",
"incorrectUsernameOrPassword": "Nesprávné uživatelské jméno nebo heslo",
"login": "Přihlášení",
"logout": "Odhlášení",
"more": "Více",
"play": "Přehrát",
"present": "Prezentovat",
"serverAddressMustBeUrl": "Adresa serveru musí být platné URL",
"serverAddressRequired": "Adresa serveru je povinná",
"serverNotFound": "Server nebyl nalezen",
"settings": "Nastavení",
"unexpectedError": "Neočekávaná chyba",
"usernameRequired": "Uživatelské jméno je povinné",
"serverAddress": "Adresa serveru",
"username": "Uživatelské jméno",
"password": "Heslo",
"playType": "Přehrát {mediaType}",
"signIn": "Přihlásit se",
"upNext": "Následující",
"libraryNotFound": "Knihovna nebyla nalezena",
"itemNotFound": "Položka nebyla nalezena",
"endsAt": "Končí v {time}",
"moreLikeThis": "Podobné",
"libraryEmpty": "Knihovna je prázdná",
"rating": "Hodnocení",
"endDate": "Datum skončení",
"releaseDate": "Datum vydání",
"alphabetically": "Abecedně"
"alphabetically": "Abecedně",
"badRequest": "Špatný požadavek. Zkuste to znovu",
"continueListening": "Pokračovat v poslechu",
"continueWatching": "Pokračovat ve sledování",
"endDate": "Datum skončení",
"endsAt": "Končí v {time}",
"episodeNumber": "Epizoda {episodeNumber}",
"home": "Domů",
"incorrectUsernameOrPassword": "Nesprávné uživatelské jméno nebo heslo",
"itemNotFound": "Položka nebyla nalezena",
"libraryEmpty": "Knihovna je prázdná",
"libraryNotFound": "Knihovna nebyla nalezena",
"login": "Přihlášení",
"logout": "Odhlášení",
"more": "Více",
"movies": "",
"password": "Heslo",
"play": "Přehrát",
"playType": "Přehrát {mediaType}",
"present": "Prezentovat",
"rating": "Hodnocení",
"releaseDate": "Datum vydání",
"serverAddress": "Adresa serveru",
"serverAddressMustBeUrl": "Adresa serveru musí být platné URL",
"serverAddressRequired": "Adresa serveru je povinná",
"serverNotFound": "Server nebyl nalezen",
"settings": "Nastavení",
"signIn": "Přihlásit se",
"unexpectedError": "Neočekávaná chyba",
"upNext": "Následující",
"username": "Uživatelské jméno",
"usernameRequired": "Uživatelské jméno je povinné",
"youMayAlsoLike": "",
"youMightLike": ""
}
+34 -32
View File
@@ -1,34 +1,36 @@
{
"badRequest": "Fehlerhafte Anfrage. Erneut versuchen",
"continueListening": "Weiter zuhören",
"continueWatching": "Weiter anschauen",
"home": "Startseite",
"incorrectUsernameOrPassword": "Ungültiger Benutzername oder Passwort",
"login": "Anmelden",
"logout": "Abmelden",
"more": "Mehr",
"play": "Abspielen",
"serverAddressMustBeUrl": "Serveradresse muss eine gültige URL sein",
"serverAddressRequired": "Serveradresse ist erforderlich",
"serverNotFound": "Server nicht gefunden",
"settings": "Einstellungen",
"unexpectedError": "Unerwarteter Fehler",
"upNext": "Als nächstes",
"usernameRequired": "Benutzername erforderlich",
"signIn": "Anmelden",
"password": "Passwort",
"username": "Benutzername",
"serverAddress": "Serveradresse",
"itemNotFound": "Element nicht gefunden",
"libraryNotFound": "Bibliothek nicht gefunden",
"episodeNumber": "Episode {episodeNumber}",
"playType": "{mediaType} abspielen",
"moreLikeThis": "Ähnliches",
"present": "Jetzt",
"endsAt": "Endet um {time}",
"libraryEmpty": "Diese Bibliothek ist leer",
"rating": "Bewertung",
"endDate": "Enddatum",
"releaseDate": "Erscheinungsdatum",
"alphabetically": "Alphabetisch"
"alphabetically": "Alphabetisch",
"badRequest": "Fehlerhafte Anfrage. Erneut versuchen",
"continueListening": "Weiter zuhören",
"continueWatching": "Weiter anschauen",
"endDate": "Enddatum",
"endsAt": "Endet um {time}",
"episodeNumber": "Episode {episodeNumber}",
"home": "Startseite",
"incorrectUsernameOrPassword": "Ungültiger Benutzername oder Passwort",
"itemNotFound": "Element nicht gefunden",
"libraryEmpty": "Diese Bibliothek ist leer",
"libraryNotFound": "Bibliothek nicht gefunden",
"login": "Anmelden",
"logout": "Abmelden",
"more": "Mehr",
"movies": "",
"password": "Passwort",
"play": "Abspielen",
"playType": "{mediaType} abspielen",
"present": "Jetzt",
"rating": "Bewertung",
"releaseDate": "Erscheinungsdatum",
"serverAddress": "Serveradresse",
"serverAddressMustBeUrl": "Serveradresse muss eine gültige URL sein",
"serverAddressRequired": "Serveradresse ist erforderlich",
"serverNotFound": "Server nicht gefunden",
"settings": "Einstellungen",
"signIn": "Anmelden",
"unexpectedError": "Unerwarteter Fehler",
"upNext": "Als nächstes",
"username": "Benutzername",
"usernameRequired": "Benutzername erforderlich",
"youMayAlsoLike": "",
"youMightLike": ""
}
+4 -1
View File
@@ -16,6 +16,7 @@
"logout": "Logout",
"more": "More",
"moreLikeThis": "More like this",
"movies": "Movies",
"password": "Password",
"play": "Play",
"playType": "Play {mediaType}",
@@ -27,9 +28,11 @@
"serverAddressRequired": "Server address is required",
"serverNotFound": "Server not found",
"settings": "Settings",
"shows": "Shows",
"signIn": "Sign in",
"unexpectedError": "Unexpected error",
"upNext": "Up next",
"username": "Username",
"usernameRequired": "Username is required"
"usernameRequired": "Username is required",
"youMayAlsoLike": "You may also like"
}
+32 -32
View File
@@ -1,34 +1,34 @@
{
"alphabetically": "Alfabéticamente",
"releaseDate": "Fecha de lanzamiento",
"endDate": "Fecha de finalización",
"rating": "Valoración",
"badRequest": "Solicitud incorrecta. Intente denuevo",
"continueListening": "Seguir escuchando",
"continueWatching": "Seguir viendo",
"episodeNumber": "Episodio {episodeNumber}",
"endsAt": "Termina a las {time}",
"home": "Inicio",
"incorrectUsernameOrPassword": "Nombre de usuario o contraseña incorrectos",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"more": "Más",
"moreLikeThis": "Similar a esto",
"play": "Reproducir",
"present": "Actualidad",
"serverAddressMustBeUrl": "La dirección del servidor debe ser una URL válida",
"serverAddressRequired": "Se requiere dirección del servidor",
"serverNotFound": "No se ha podido encontrar el servidor",
"settings": "Ajustes",
"unexpectedError": "Error desconocido",
"usernameRequired": "Se requiere nombre de usuario",
"serverAddress": "Dirección del servidor",
"username": "Nombre de usuario",
"password": "Contraseña",
"playType": "Reproducir {mediaType}",
"signIn": "Iniciar sesión",
"upNext": "A continuación",
"libraryNotFound": "Librería no encontrada",
"itemNotFound": "Articulo no encontrado",
"libraryEmpty": "Esta librería esta vacía"
"alphabetically": "Alfabéticamente",
"releaseDate": "Fecha de lanzamiento",
"endDate": "Fecha de finalización",
"rating": "Valoración",
"badRequest": "Solicitud incorrecta. Intente denuevo",
"continueListening": "Seguir escuchando",
"continueWatching": "Seguir viendo",
"episodeNumber": "Episodio {episodeNumber}",
"endsAt": "Termina a las {time}",
"home": "Inicio",
"incorrectUsernameOrPassword": "Nombre de usuario o contraseña incorrectos",
"login": "Iniciar sesión",
"logout": "Cerrar sesión",
"more": "Más",
"moreLikeThis": "Similar a esto",
"play": "Reproducir",
"present": "Actualidad",
"serverAddressMustBeUrl": "La dirección del servidor debe ser una URL válida",
"serverAddressRequired": "Se requiere dirección del servidor",
"serverNotFound": "No se ha podido encontrar el servidor",
"settings": "Ajustes",
"unexpectedError": "Error desconocido",
"usernameRequired": "Se requiere nombre de usuario",
"serverAddress": "Dirección del servidor",
"username": "Nombre de usuario",
"password": "Contraseña",
"playType": "Reproducir {mediaType}",
"signIn": "Iniciar sesión",
"upNext": "A continuación",
"libraryNotFound": "Librería no encontrada",
"itemNotFound": "Articulo no encontrado",
"libraryEmpty": "Esta librería esta vacía"
}
+32 -32
View File
@@ -1,34 +1,34 @@
{
"badRequest": "Solicitud incorrecta. Reintente",
"continueListening": "Continuar escuchando",
"continueWatching": "Continuar viendo",
"episodeNumber": "Episodio {episodeNumber}",
"home": "Inicio",
"incorrectUsernameOrPassword": "Usuario o contraseña incorrecta",
"login": "Ingresar",
"logout": "Salir",
"more": "Más",
"play": "Reproducir",
"serverAddressMustBeUrl": "Dirección del servidor debe ser una URL valida",
"serverAddressRequired": "Dirección del servidor requerida",
"serverNotFound": "Servidor no encontrado",
"settings": "Configuración",
"unexpectedError": "Error inesperado",
"usernameRequired": "Se requiere nombre de usuario",
"serverAddress": "Dirección del servidor",
"username": "Usuario",
"password": "Contraseña",
"playType": "Reproducir {mediaType}",
"signIn": "Ingresar",
"upNext": "Siguiente",
"libraryNotFound": "Librería no encontrada",
"itemNotFound": "Item no encontrado",
"present": "Presente",
"libraryEmpty": "La biblioteca está vacía",
"moreLikeThis": "Más como esto",
"endsAt": "Termina {time}",
"rating": "Calificación",
"endDate": "Fecha de finalización",
"releaseDate": "Fecha de estreno",
"alphabetically": "Alfabéticamente"
"badRequest": "Solicitud incorrecta. Reintente",
"continueListening": "Continuar escuchando",
"continueWatching": "Continuar viendo",
"episodeNumber": "Episodio {episodeNumber}",
"home": "Inicio",
"incorrectUsernameOrPassword": "Usuario o contraseña incorrecta",
"login": "Ingresar",
"logout": "Salir",
"more": "Más",
"play": "Reproducir",
"serverAddressMustBeUrl": "Dirección del servidor debe ser una URL valida",
"serverAddressRequired": "Dirección del servidor requerida",
"serverNotFound": "Servidor no encontrado",
"settings": "Configuración",
"unexpectedError": "Error inesperado",
"usernameRequired": "Se requiere nombre de usuario",
"serverAddress": "Dirección del servidor",
"username": "Usuario",
"password": "Contraseña",
"playType": "Reproducir {mediaType}",
"signIn": "Ingresar",
"upNext": "Siguiente",
"libraryNotFound": "Librería no encontrada",
"itemNotFound": "Item no encontrado",
"present": "Presente",
"libraryEmpty": "La biblioteca está vacía",
"moreLikeThis": "Más como esto",
"endsAt": "Termina {time}",
"rating": "Calificación",
"endDate": "Fecha de finalización",
"releaseDate": "Fecha de estreno",
"alphabetically": "Alfabéticamente"
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Kirjaudu ulos",
"more": "Lisää",
"moreLikeThis": "Lisää samanlaisia",
"movies": "",
"password": "Salasana",
"play": "Toista",
"playType": "Toista {mediaType}",
@@ -30,5 +31,6 @@
"unexpectedError": "Odottamaton virhe",
"upNext": "Seuraava",
"username": "Käyttäjätunnus",
"usernameRequired": "Käyttäjänimi vaaditaan"
"usernameRequired": "Käyttäjänimi vaaditaan",
"youMayAlsoLike": ""
}
+34 -32
View File
@@ -1,34 +1,36 @@
{
"badRequest": "Mauvaise requête. Réessayez",
"continueListening": "Poursuivre l'écoute",
"continueWatching": "Poursuivre le visionnage",
"home": "Accueil",
"incorrectUsernameOrPassword": "Nom d'utilisateur ou mot de passe incorrect",
"login": "Connexion",
"logout": "Se déconnecter",
"more": "Plus",
"play": "Lire",
"serverAddressMustBeUrl": "L'adresse du serveur doit être une URL valide",
"serverAddressRequired": "L'adresse du serveur est requise",
"serverNotFound": "Serveur introuvable",
"settings": "Paramètres",
"unexpectedError": "Erreur inattendue",
"usernameRequired": "Nom d'utilisateur requis",
"serverAddress": "Adresse du serveur",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"signIn": "Se connecter",
"upNext": "À suivre",
"libraryNotFound": "Médiathèque introuvable",
"itemNotFound": "Élément introuvable",
"episodeNumber": "Épisode {episodeNumber}",
"playType": "Lire {mediaType}",
"present": "Maintenant",
"endsAt": "Se termine à {time}",
"moreLikeThis": "Similaires",
"libraryEmpty": "Cette médiathèque est vide",
"rating": "Critique",
"endDate": "Date de fin",
"releaseDate": "Date de sortie",
"alphabetically": "Alphabétique"
"alphabetically": "Alphabétique",
"badRequest": "Mauvaise requête. Réessayez",
"continueListening": "Poursuivre l'écoute",
"continueWatching": "Poursuivre le visionnage",
"endDate": "Date de fin",
"endsAt": "Se termine à {time}",
"episodeNumber": "Épisode {episodeNumber}",
"home": "Accueil",
"incorrectUsernameOrPassword": "Nom d'utilisateur ou mot de passe incorrect",
"itemNotFound": "Élément introuvable",
"libraryEmpty": "Cette médiathèque est vide",
"libraryNotFound": "Médiathèque introuvable",
"login": "Connexion",
"logout": "Se déconnecter",
"more": "Plus",
"movies": "",
"password": "Mot de passe",
"play": "Lire",
"playType": "Lire {mediaType}",
"present": "Maintenant",
"rating": "Critique",
"releaseDate": "Date de sortie",
"serverAddress": "Adresse du serveur",
"serverAddressMustBeUrl": "L'adresse du serveur doit être une URL valide",
"serverAddressRequired": "L'adresse du serveur est requise",
"serverNotFound": "Serveur introuvable",
"settings": "Paramètres",
"signIn": "Se connecter",
"unexpectedError": "Erreur inattendue",
"upNext": "À suivre",
"username": "Nom d'utilisateur",
"usernameRequired": "Nom d'utilisateur requis",
"youMayAlsoLike": "",
"youMightLike": ""
}
+34 -32
View File
@@ -1,34 +1,36 @@
{
"badRequest": "Bloga užklausa. Bandykite iš naujo",
"continueListening": "Tęsti klausymą",
"continueWatching": "Tęsti žiūrėjimą",
"episodeNumber": "Epizodas nr. {episodeNumber}",
"endsAt": "Baigiasi {time}",
"home": "Pradžia",
"incorrectUsernameOrPassword": "Neteisingas vartotojo vardas arba slaptažodis",
"login": "Prisijungti",
"logout": "Atsijungti",
"more": "Daugiau",
"moreLikeThis": "Daugiau panašių",
"play": "Leisti",
"present": "Dabar",
"serverAddressMustBeUrl": "Serverio adresas turi būti teisingas URL",
"serverAddressRequired": "Serverio adresas yra būtinas",
"serverNotFound": "Serveris nerastas",
"settings": "Nustatymai",
"unexpectedError": "Netikėta klaida",
"usernameRequired": "Vartotojo vardas yra būtinas",
"serverAddress": "Serverio adresas",
"username": "Vartotojo vardas",
"password": "Slaptažodis",
"playType": "Leisti {mediaType}",
"signIn": "Prisijungti",
"upNext": "Toliau",
"libraryNotFound": "Mediateka nerasta",
"itemNotFound": "Elementas nerastas",
"libraryEmpty": "Tuščia mediateka",
"alphabetically": "Paraidžiui",
"releaseDate": "Išleidimo data",
"endDate": "Pabaigos data",
"rating": "Įvertinimas"
"alphabetically": "Paraidžiui",
"badRequest": "Bloga užklausa. Bandykite iš naujo",
"continueListening": "Tęsti klausymą",
"continueWatching": "Tęsti žiūrėjimą",
"endDate": "Pabaigos data",
"endsAt": "Baigiasi {time}",
"episodeNumber": "Epizodas nr. {episodeNumber}",
"home": "Pradžia",
"incorrectUsernameOrPassword": "Neteisingas vartotojo vardas arba slaptažodis",
"itemNotFound": "Elementas nerastas",
"libraryEmpty": "Tuščia mediateka",
"libraryNotFound": "Mediateka nerasta",
"login": "Prisijungti",
"logout": "Atsijungti",
"more": "Daugiau",
"movies": "",
"password": "Slaptažodis",
"play": "Leisti",
"playType": "Leisti {mediaType}",
"present": "Dabar",
"rating": "Įvertinimas",
"releaseDate": "Išleidimo data",
"serverAddress": "Serverio adresas",
"serverAddressMustBeUrl": "Serverio adresas turi būti teisingas URL",
"serverAddressRequired": "Serverio adresas yra būtinas",
"serverNotFound": "Serveris nerastas",
"settings": "Nustatymai",
"signIn": "Prisijungti",
"unexpectedError": "Netikėta klaida",
"upNext": "Toliau",
"username": "Vartotojo vardas",
"usernameRequired": "Vartotojo vardas yra būtinas",
"youMayAlsoLike": "",
"youMightLike": ""
}
+32 -32
View File
@@ -1,34 +1,34 @@
{
"badRequest": "Feil i forespørselen. Prøv igjen",
"continueListening": "Fortsett å lytte",
"continueWatching": "Fortsett å se",
"home": "Hjem",
"incorrectUsernameOrPassword": "Feil brukernavn eller passord",
"login": "Logg inn",
"logout": "Logg ut",
"more": "Mer",
"play": "Spill av",
"serverAddressMustBeUrl": "Server adressen må være en gyldig link",
"serverAddressRequired": "Serveradresse er påkrevd",
"serverNotFound": "Server ikke funnet",
"settings": "Innstillinger",
"unexpectedError": "Uventet feil",
"usernameRequired": "Brukernavn er påkrevd",
"serverAddress": "Serveradresse",
"username": "Brukernavn",
"password": "Passord",
"signIn": "Logg inn",
"upNext": "Neste",
"libraryNotFound": "Bibliotek ikke funnet",
"itemNotFound": "Gjenstand ikke funnet",
"episodeNumber": "Episode {episodeNumber}",
"playType": "Spill av {mediaType}",
"libraryEmpty": "Dette biblioteket er tomt",
"present": "Tilstede",
"moreLikeThis": "Mer som dette",
"endsAt": "Slutter {Time}",
"rating": "Vurdering",
"endDate": "Sluttdato",
"releaseDate": "Slippdato",
"alphabetically": "Alfabetisk"
"badRequest": "Feil i forespørselen. Prøv igjen",
"continueListening": "Fortsett å lytte",
"continueWatching": "Fortsett å se",
"home": "Hjem",
"incorrectUsernameOrPassword": "Feil brukernavn eller passord",
"login": "Logg inn",
"logout": "Logg ut",
"more": "Mer",
"play": "Spill av",
"serverAddressMustBeUrl": "Server adressen må være en gyldig link",
"serverAddressRequired": "Serveradresse er påkrevd",
"serverNotFound": "Server ikke funnet",
"settings": "Innstillinger",
"unexpectedError": "Uventet feil",
"usernameRequired": "Brukernavn er påkrevd",
"serverAddress": "Serveradresse",
"username": "Brukernavn",
"password": "Passord",
"signIn": "Logg inn",
"upNext": "Neste",
"libraryNotFound": "Bibliotek ikke funnet",
"itemNotFound": "Gjenstand ikke funnet",
"episodeNumber": "Episode {episodeNumber}",
"playType": "Spill av {mediaType}",
"libraryEmpty": "Dette biblioteket er tomt",
"present": "Tilstede",
"moreLikeThis": "Mer som dette",
"endsAt": "Slutter {Time}",
"rating": "Vurdering",
"endDate": "Sluttdato",
"releaseDate": "Slippdato",
"alphabetically": "Alfabetisk"
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Uitloggen",
"more": "Meer",
"moreLikeThis": "Meer zoals dit",
"movies": "",
"password": "Wachtwoord",
"play": "Afspelen",
"playType": "Speel {mediaType}",
@@ -30,5 +31,6 @@
"unexpectedError": "Onverwachte fout",
"upNext": "Volgende",
"username": "Gebruikersnaam",
"usernameRequired": "Gebruikersnaam is verplicht"
"usernameRequired": "Gebruikersnaam is verplicht",
"youMayAlsoLike": ""
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Wyloguj",
"more": "Więcej",
"moreLikeThis": "",
"movies": "",
"password": "Hasło",
"play": "Odtwarzaj",
"playType": "Odtwarzaj {mediaType}",
@@ -30,5 +31,6 @@
"unexpectedError": "Niespodziewany błąd",
"upNext": "Następnie",
"username": "Nazwa użytkownika",
"usernameRequired": "Nazwa użytkownika jest wymagana"
"usernameRequired": "Nazwa użytkownika jest wymagana",
"youMayAlsoLike": ""
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Deconectare",
"more": "Mai mult",
"moreLikeThis": "Similare",
"movies": "",
"password": "Parolă",
"play": "Rulează",
"playType": "Redă {mediaType}",
@@ -30,5 +31,6 @@
"unexpectedError": "Eroare neașteptată",
"upNext": "Urmează",
"username": "Utilizator",
"usernameRequired": "Numele de utilizator este necesar"
"usernameRequired": "Numele de utilizator este necesar",
"youMayAlsoLike": ""
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Odhlásenie",
"more": "Viac",
"moreLikeThis": "Podobné",
"movies": "",
"password": "Heslo",
"play": "Prehrať",
"playType": "Prehrať {mediaType}",
@@ -30,5 +31,6 @@
"unexpectedError": "Neočakávaná chyba",
"upNext": "Nasleduje",
"username": "Používateľské meno",
"usernameRequired": "Používateľské meno je požadované"
"usernameRequired": "Používateľské meno je požadované",
"youMayAlsoLike": ""
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Odjava",
"more": "Več",
"moreLikeThis": "",
"movies": "",
"password": "Geslo",
"play": "Predvajaj",
"playType": "",
@@ -30,5 +31,6 @@
"unexpectedError": "Nepričakovana napaka",
"upNext": "Naslednje",
"username": "Uporabniško ime",
"usernameRequired": "Uporabniško ime je zahtevano"
"usernameRequired": "Uporabniško ime je zahtevano",
"youMayAlsoLike": ""
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Logga ut",
"more": "Mer",
"moreLikeThis": "Mer som detta",
"movies": "",
"password": "Lösenord",
"play": "Spela",
"playType": "Spela {mediaType}",
@@ -30,5 +31,6 @@
"unexpectedError": "Oförutsett fel",
"upNext": "Kommer näst",
"username": "Användarnamn",
"usernameRequired": "Användarnamn krävs"
"usernameRequired": "Användarnamn krävs",
"youMayAlsoLike": ""
}
+34 -32
View File
@@ -1,34 +1,36 @@
{
"badRequest": "தவறான கோரிக்கை. மீண்டும் முயற்சி செய்யவும்",
"continueListening": "தொடர்ந்து கேளுங்கள்",
"continueWatching": "தொடர்ந்து பார்த்துக் கொள்ளுங்கள்",
"home": "முகப்பு",
"incorrectUsernameOrPassword": "தவறான பயனர்பெயர் அல்லது கடவுச்சொல்",
"login": "புகுபதிகை",
"logout": "விடுபதிகை",
"more": "மேலும்",
"play": "இயக்கு",
"serverAddressMustBeUrl": "சேவையக முகவரி சரியான URL ஆக இருக்க வேண்டும்",
"serverAddressRequired": "சேவையக முகவரி தேவை",
"serverNotFound": "சேவையகம் கிடைக்கவில்லை",
"settings": "அமைப்புகள்",
"unexpectedError": "எதிர்பாராத பிழை",
"usernameRequired": "பயனர்பெயர் தேவை",
"serverAddress": "சேவையக முகவரி",
"username": "பயனர்பெயர்",
"password": "கடவுச்சொல்",
"signIn": "உள்நுழைக",
"upNext": "அடுத்தது",
"libraryNotFound": "நூலகம் கிடைக்கவில்லை",
"itemNotFound": "பொருகிடைக்கவில்லை",
"episodeNumber": "அத்தியாயம் {episodeNumber}",
"playType": "{mediaType} ஐ இயக்கு",
"present": "தற்போது",
"moreLikeThis": "மேலும் இது போன்றது",
"endsAt": "{time} இல் முடிகிறது",
"libraryEmpty": "இந்த நூலகம் காலியாக உள்ளது",
"rating": "தி்பீடு",
"endDate": "கடைசி தேதி",
"releaseDate": "வெளிவரும் தேதி",
"alphabetically": "அகர வரிசைப்படி"
"alphabetically": "அகர வரிசைப்படி",
"badRequest": "தவறான கோரிக்கை. மீண்டும் முயற்சி செய்யவும்",
"continueListening": "தொடர்ந்து கேளுங்கள்",
"continueWatching": "தொடர்ந்து பார்த்துக் கொள்ளுங்கள்",
"endDate": "கடைசி தேதி",
"endsAt": "{time} இல் முடிகிறது",
"episodeNumber": "அத்தியாயம் {episodeNumber}",
"home": "முகப்பு",
"incorrectUsernameOrPassword": "தவறான பயனர்பெயர் அல்லது கடவுச்சொல்",
"itemNotFound": "பொருள் கிடைக்கவில்லை",
"libraryEmpty": "இந்த நூலகம் காலியாக உள்ளது",
"libraryNotFound": "நூலகம் கிடைக்கவில்லை",
"login": "புகுபதிகை",
"logout": "விடுபதிகை",
"more": "மேலும்",
"movies": "",
"password": "கடவுச்சொல்",
"play": "இயக்கு",
"playType": "{mediaType} ஐ இயக்கு",
"present": "தற்போது",
"rating": "மதிப்பீடு",
"releaseDate": "வெளிவருதேதி",
"serverAddress": "சேவையக முகவரி",
"serverAddressMustBeUrl": "சேவையக முகவரி சரியான URL ஆக இருக்க வேண்டும்",
"serverAddressRequired": "சேவையக முகவரி தேவை",
"serverNotFound": "சேவையகம் கிடைக்கவில்லை",
"settings": "அமைப்புகள்",
"signIn": "உள்நுழைக",
"unexpectedError": "தி்பாராத பிழை",
"upNext": "அடுத்தது",
"username": "பயனர்பெயர்",
"usernameRequired": "பயனர்பெயர் தேவை",
"youMayAlsoLike": "",
"youMightLike": ""
}
+3 -1
View File
@@ -15,6 +15,7 @@
"logout": "Çıkış",
"more": "Dahası",
"moreLikeThis": "Buna Benzer Daha Fazla",
"movies": "",
"password": "Şifre",
"play": "Oynat",
"playType": "",
@@ -30,5 +31,6 @@
"unexpectedError": "Beklenmedik hata",
"upNext": "Sonraki",
"username": "Kullanıcı Adı",
"usernameRequired": "Kullanıcı adı gerekli"
"usernameRequired": "Kullanıcı adı gerekli",
"youMayAlsoLike": ""
}
+34 -32
View File
@@ -1,34 +1,36 @@
{
"badRequest": "Yêu cầu đã thất bại. Thử lại",
"continueListening": "Tiếp tục nghe",
"continueWatching": "Tiếp tục xem",
"episodeNumber": "Tập {episodeNumber}",
"home": "Trang chủ",
"incorrectUsernameOrPassword": "Sai tên đăng nhập hoặc mật khẩu",
"login": "Đăng nhập",
"logout": "Thoát",
"more": "Thêm",
"play": "Phát",
"present": "Hiện tại",
"serverAddressMustBeUrl": "Địa chỉ máy chủ phải là một URL hợp lệ",
"serverAddressRequired": "Địa chỉ máy chủ là bắt buộc",
"serverNotFound": "Không tìm thấy máy chủ",
"settings": "Cài đặt",
"unexpectedError": "Lỗi không mong muốn",
"usernameRequired": "Tên đăng nhập là bắt buộc",
"serverAddress": "Địa chỉ máy chủ",
"username": "Tên đăng nhập",
"password": "Mật khẩu",
"playType": "Phát {mediaType}",
"signIn": "Đăng nhập",
"upNext": "Tiếp theo",
"libraryNotFound": "Không tìm thấy thư viện",
"itemNotFound": "Không tìm thấy mục",
"moreLikeThis": "Thêm tương tự như thế này",
"endsAt": "Kết thúc lúc {time}",
"libraryEmpty": "Thư viện này trống",
"rating": "Xếp hạng",
"endDate": "Ngày kết thúc",
"releaseDate": "Ngày phát hành",
"alphabetically": "Theo bảng chữ cái"
"alphabetically": "Theo bảng chữ cái",
"badRequest": "Yêu cầu đã thất bại. Thử lại",
"continueListening": "Tiếp tục nghe",
"continueWatching": "Tiếp tục xem",
"endDate": "Ngày kết thúc",
"endsAt": "Kết thúc lúc {time}",
"episodeNumber": "Tập {episodeNumber}",
"home": "Trang chủ",
"incorrectUsernameOrPassword": "Sai tên đăng nhập hoặc mật khẩu",
"itemNotFound": "Không tìm thấy mục",
"libraryEmpty": "Thư viện này trống",
"libraryNotFound": "Không tìm thấy thư viện",
"login": "Đăng nhập",
"logout": "Thoát",
"more": "Thêm",
"movies": "",
"password": "Mật khẩu",
"play": "Phát",
"playType": "Phát {mediaType}",
"present": "Hiện tại",
"rating": "Xếp hạng",
"releaseDate": "Ngày phát hành",
"serverAddress": "Địa chỉ máy chủ",
"serverAddressMustBeUrl": "Địa chỉ máy chủ phải là một URL hợp lệ",
"serverAddressRequired": "Địa chỉ máy chủ là bắt buộc",
"serverNotFound": "Không tìm thấy máy chủ",
"settings": "Cài đặt",
"signIn": "Đăng nhập",
"unexpectedError": "Lỗi không mong muốn",
"upNext": "Tiếp theo",
"username": "Tên đăng nhập",
"usernameRequired": "Tên đăng nhập là bắt buộc",
"youMayAlsoLike": "",
"youMightLike": ""
}
+34 -32
View File
@@ -1,34 +1,36 @@
{
"badRequest": "错误的请求。请重试",
"continueListening": "继续听",
"continueWatching": "继续观看",
"episodeNumber": "章节 {episodeNumber}",
"home": "首页",
"incorrectUsernameOrPassword": "用户名或密码错误",
"login": "登陆",
"logout": "注销",
"more": "更多",
"play": "播放",
"serverAddressMustBeUrl": "服务器地址必须是有效的网址",
"serverAddressRequired": "服务器地址为必填项",
"serverNotFound": "找不到服务器",
"settings": "设置",
"unexpectedError": "意外的错误",
"usernameRequired": "用户名为必填项",
"serverAddress": "服务器地址",
"username": "用户名",
"password": "密码",
"signIn": "登陆",
"upNext": "下一个",
"libraryNotFound": "未找到媒体库",
"itemNotFound": "未找到项目",
"playType": "播放 {mediaType}",
"present": "现在",
"moreLikeThis": "更像这样",
"libraryEmpty": "这个库是空的",
"endsAt": "结束于 {time}",
"rating": "评价",
"endDate": "结束日期",
"releaseDate": "发布日期",
"alphabetically": "按字母排序"
"alphabetically": "按字母排序",
"badRequest": "错误的请求。请重试",
"continueListening": "继续",
"continueWatching": "继续观看",
"endDate": "结束日期",
"endsAt": "结束于 {time}",
"episodeNumber": "章节 {episodeNumber}",
"home": "首页",
"incorrectUsernameOrPassword": "用户名或密码错误",
"itemNotFound": "未找到项目",
"libraryEmpty": "这个库是空的",
"libraryNotFound": "未找到媒体库",
"login": "登陆",
"logout": "注销",
"more": "更多",
"movies": "",
"password": "密码",
"play": "播放",
"playType": "播放 {mediaType}",
"present": "现在",
"rating": "评价",
"releaseDate": "发布日期",
"serverAddress": "服务器地址",
"serverAddressMustBeUrl": "服务器地址必须是有效的网址",
"serverAddressRequired": "服务器地址为必填项",
"serverNotFound": "找不到服务器",
"settings": "设置",
"signIn": "登陆",
"unexpectedError": "意外的错误",
"upNext": "下一个",
"username": "用户名",
"usernameRequired": "用户名为必填项",
"youMayAlsoLike": "",
"youMightLike": ""
}
+5 -2
View File
@@ -19,7 +19,7 @@ declare module '@nuxt/types' {
declare module 'vue/types/vue' {
interface Vue {
ticksToMs: (ticks: number) => number;
ticksToMs: (ticks: number | null | undefined) => number;
msToTicks: (ms: number) => number;
}
}
@@ -32,7 +32,10 @@ const timeUtils = Vue.extend({
* @param {string} ticks number of ticks to convert from
* @returns {number} Correct number of ms
*/
ticksToMs(ticks: number): number {
ticksToMs(ticks: number | null | undefined): number {
if (!ticks) {
ticks = 0;
}
return ticks / 10000;
},
/**
+3 -1
View File
@@ -63,7 +63,8 @@ const config: NuxtConfig = {
'@nuxt/typescript-build',
// Doc: https://github.com/nuxt-community/stylelint-module
'@nuxtjs/stylelint-module',
'@nuxtjs/vuetify'
'@nuxtjs/vuetify',
'@nuxtjs/date-fns'
],
/*
** Nuxt.js modules
@@ -176,6 +177,7 @@ const config: NuxtConfig = {
*/
vuetify: {
customVariables: ['~/assets/variables.scss'],
treeShake: true,
defaultAssets: false,
theme: {
dark: true,
+3
View File
@@ -31,6 +31,8 @@
"@types/lodash": "^4.14.162",
"@types/uuid": "^8.3.0",
"blurhash": "^1.1.3",
"fontsource-noto-sans": "^3.0.10",
"langs": "^2.0.0",
"lodash": "^4.17.20",
"mux.js": "^5.6.7",
"nuxt": "^2.14.7",
@@ -48,6 +50,7 @@
"@mdi/font": "^5.7.55",
"@nuxt/types": "^2.14.7",
"@nuxt/typescript-build": "^2.0.2",
"@nuxtjs/date-fns": "^1.4.0",
"@nuxtjs/eslint-config": "^4.0.0",
"@nuxtjs/eslint-config-typescript": "^4.0.0",
"@nuxtjs/eslint-module": "^3.0.0",
+363 -126
View File
@@ -1,72 +1,370 @@
<template>
<v-container fluid class="pa-0 item-container">
<v-img
v-resize="updateBackdropImage"
:src="backdropImageSource"
class="d-flex align-end backdrop-image"
max-width="100%"
>
<div class="d-flex align-end gradient-container">
<div class="d-flex flex-wrap item-details-container">
<div class="white--text">
<v-img
v-if="
item.ImageTags && item.ImageTags.Logo && getAspectRatio() > 1
"
:src="getImageUrl(item.Id, 'Logo')"
contain
:alt="item.Name"
max-width="50%"
class="mb-4"
></v-img>
<h1 v-else>{{ item.Name }}</h1>
<div class="item-sub-heading">{{ renderItemSubHeading() }}</div>
<p class="item-overview">{{ item.Overview }}</p>
</div>
<div class="item-details-right">
<v-btn
class="play-button"
color="primary"
:to="`./${item.Id}/play`"
>{{ $t('playType', { mediaType: item.Type }) }}</v-btn
<v-container class="ml-3 mr-3">
<v-row>
<v-col cols="9">
<v-row>
<v-col cols="3">
<card v-if="loaded" :item="item" no-text no-margin />
<skeleton-card v-else no-text />
</v-col>
<v-col cols="9">
<h1 v-if="loaded" class="text-h4 font-weight-light text-truncate">
{{ item.Name }}
</h1>
<v-skeleton-loader
v-else
class="mt-3 mb-2"
type="heading"
width="50em"
/>
<h2
v-if="loaded && item.OriginalTitle"
class="text-subtitle-1 text-truncate"
>
<v-btn>{{ $t('more') }}</v-btn>
</div>
{{ item.OriginalTitle }}
</h2>
<v-skeleton-loader v-else type="heading" width="25em" />
<div class="text-caption text-h4 font-weight-medium">
<media-info
v-if="loaded"
:item="item"
year
runtime
rating
ends-at
/>
<v-skeleton-loader v-else type="text" width="50em" class="mt-2" />
</div>
<div class="mt-3 mb-2">
<v-btn
v-if="loaded"
class="play-button mr-2"
color="primary"
min-width="8em"
:disabled="isPlayable"
depressed
rounded
:to="`./${item.Id}/play`"
>{{ $t('play') }}</v-btn
>
<v-skeleton-loader v-else type="button" />
<v-btn v-if="loaded" outlined icon>
<v-icon>mdi-dots-horizontal</v-icon>
</v-btn>
</div>
<v-col class="mt-2" cols="10">
<v-row
v-if="loaded && item && item.Genres && item.Genres.length > 1"
>
<v-col cols="2" class="d-flex align-center pa-0">
<label class="text--secondary">Genres</label>
</v-col>
<v-col cols="7">{{ item.Genres.join(', ') }}</v-col>
</v-row>
<div
v-if="
loaded &&
item &&
((item.MediaSources && item.MediaSources.length > 1) ||
videoTracks.length > 0 ||
audioTracks.length > 0 ||
subtitleTracks.length > 0)
"
class="mt-2"
>
<v-row v-if="item.MediaSources.length > 1">
<v-col cols="2" class="d-flex align-center pa-0">
<label class="text--secondary">Video</label>
</v-col>
<v-col cols="7">
<v-select
v-model="currentSource"
:items="item.MediaSources"
outlined
filled
flat
dense
single-line
hide-details
>
<template slot="selection" slot-scope="{ item }">
{{ item.DisplayTitle }}
</template>
</v-select>
</v-col>
</v-row>
<v-row v-if="videoTracks.length > 0">
<v-col cols="2" class="d-flex align-center pa-0">
<label class="text--secondary">Video</label>
</v-col>
<v-col cols="7">
<v-select
v-model="currentVideoTrack"
:items="videoTracks"
:disabled="videoTracks.length <= 1"
outlined
filled
flat
dense
single-line
hide-details
>
<template slot="selection" slot-scope="{ item }">
{{ item.DisplayTitle }}
</template>
</v-select>
</v-col>
</v-row>
<v-row v-if="audioTracks.length > 0">
<v-col cols="2" class="d-flex align-center pa-0">
<label class="text--secondary">Audio</label>
</v-col>
<v-col cols="7">
<v-select
v-if="audioTracks.length > 1"
v-model="currentAudioTrack"
:items="audioTracks"
:disabled="audioTracks.length <= 1"
outlined
filled
flat
dense
single-line
hide-details
>
<template slot="selection" slot-scope="{ item }">
{{ item.DisplayTitle }}
</template>
<template slot="item" slot-scope="{ item, on, attrs }">
<v-list-item v-bind="attrs" two-line v-on="on">
<v-list-item-avatar>
<v-icon
v-text="getSurroundIcon(item.ChannelLayout)"
></v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>{{
item.Title
}}</v-list-item-title>
<v-list-item-subtitle>
{{ getLanguageName(item.Language) }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
</v-select>
</v-col>
</v-row>
<v-row v-if="subtitleTracks.length > 0">
<v-col cols="2" class="d-flex align-center pa-0">
<label class="text--secondary">Subtitles</label>
</v-col>
<v-col cols="7">
<v-select
v-if="subtitleTracks.length > 0"
v-model="currentSubtitleTrack"
:items="subtitleTracks"
outlined
filled
flat
dense
single-line
hide-details
>
<template slot="selection" slot-scope="{ item }">
{{ item.DisplayTitle }}
</template>
<template slot="item" slot-scope="{ item, on, attrs }">
<v-list-item v-bind="attrs" two-line v-on="on">
<v-list-item-content>
<v-list-item-title>{{
item.Title
}}</v-list-item-title>
<v-list-item-subtitle>
{{ getLanguageName(item.Language) }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
</v-select>
</v-col>
</v-row>
</div>
</v-col>
<div>
<p
v-if="loaded && item.Taglines"
class="text-subtitle-1 text-truncate"
>
{{ item.Taglines[0] }}
</p>
<v-skeleton-loader v-else type="text" width="25em" class="mb-4" />
<p v-if="loaded" class="item-overview">{{ item.Overview }}</p>
<div v-else>
<v-skeleton-loader
v-for="index in 2"
:key="index"
type="sentences"
/>
</div>
</div>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<related-items
v-if="item.Type !== 'Series'"
:id="$route.params.itemId"
/>
<season-tabs
v-if="item.Type === 'Series'"
:item="item"
></season-tabs>
</v-col>
</v-row>
</v-col>
<v-col cols="3">
<div v-if="crew.length > 0">
<h2 v-if="loaded">Crew</h2>
<v-skeleton-loader v-else type="heading" />
<person-list :items="crew" :skeleton-length="3" />
</div>
</div>
</v-img>
<season-tabs v-if="item.Type === 'Series'" :item="item"></season-tabs>
<related-items :id="$route.params.itemId" />
<div v-if="actors.length > 0">
<h2 v-if="loaded">Cast</h2>
<v-skeleton-loader v-else type="heading" />
<person-list :items="actors" :skeleton-length="5" />
</div>
<related-items
v-if="item.Type === 'Series'"
:id="$route.params.itemId"
vertical
/>
</v-col>
</v-row>
</v-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { BaseItemDto } from '~/api';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment -- Temporary module while waiting for fixes to language names on the server
// @ts-ignore
import langs from 'langs';
import {
BaseItemDto,
BaseItemPerson,
MediaSourceInfo,
MediaStream
} from '~/api';
import imageHelper from '~/mixins/imageHelper';
import timeUtils from '~/mixins/timeUtils';
export default Vue.extend({
mixins: [imageHelper, timeUtils],
mixins: [imageHelper],
data() {
return {
loaded: false,
item: {} as BaseItemDto,
backdropImageSource: ''
backdropImageSource: '',
currentSource: {} as MediaSourceInfo,
videoTracks: [] as MediaStream[],
currentVideoTrack: {} as MediaStream,
audioTracks: [] as MediaStream[],
currentAudioTrack: {} as MediaStream,
subtitleTracks: [] as MediaStream[],
currentSubtitleTrack: {} as MediaStream
};
},
computed: {
isPlayable: {
get() {
// TODO: Move this to a mixin
if (['Movie'].includes(this.$data.item.Type)) {
return false;
} else {
return true;
}
}
},
crew: {
get() {
if (this.$data.item.People) {
// TODO: Figure out how common it is to have more than one director
return this.$data.item.People.filter((person: BaseItemPerson) => {
return ['Director', 'Writer'].includes(person.Type || '');
});
} else {
return [];
}
}
},
actors: {
get() {
if (this.$data.item.People) {
return this.$data.item.People.filter((person: BaseItemPerson) => {
return person.Type === 'Actor';
}).slice(0, 10);
} else {
return [];
}
}
}
},
async beforeMount() {
const items = (
await this.$api.items.getItems({
uId: this.$auth.user.Id,
const item = (
await this.$api.userLibrary.getItem({
userId: this.$auth.user.Id,
ids: this.$route.params.itemId,
fields: 'Overview,Genres'
itemId: this.$route.params.itemId
})
).data.Items;
).data;
if (items) {
this.$store.dispatch('backdrop/set', { item: items[0] });
this.item = items[0];
if (item) {
this.$store.dispatch('backdrop/set', { item });
if (item.MediaSources) {
this.currentSource = item.MediaSources[0];
// Filter the streams to get each type of track
if (this.currentSource.MediaStreams) {
this.videoTracks = this.currentSource.MediaStreams.filter(
(stream: MediaStream) => {
return stream.Type === 'Video';
}
);
this.audioTracks = this.currentSource.MediaStreams.filter(
(stream: MediaStream) => {
return stream.Type === 'Audio';
}
);
this.subtitleTracks = this.currentSource.MediaStreams.filter(
(stream: MediaStream) => {
return stream.Type === 'Subtitle';
}
);
// Set default tracks
if (this.videoTracks.length > 0) {
this.currentVideoTrack = this.videoTracks[0];
}
if (
this.audioTracks.length > 0 &&
this.currentSource.DefaultAudioStreamIndex
) {
this.currentAudioTrack = this.audioTracks[
this.currentSource.DefaultAudioStreamIndex
];
}
if (
this.subtitleTracks.length > 0 &&
this.currentSource.DefaultSubtitleStreamIndex
) {
this.currentSubtitleTrack = this.subtitleTracks[
this.currentSource.DefaultSubtitleStreamIndex
];
}
}
}
this.item = item;
this.loaded = true;
}
this.updateBackdropImage();
@@ -75,8 +373,22 @@ export default Vue.extend({
this.$store.dispatch('backdrop/clear');
},
methods: {
getAspectRatio() {
return window.innerWidth / window.innerHeight;
getLanguageName(code: string) {
return langs.where('2B', code).name;
},
getSurroundIcon(layout: string) {
switch (layout) {
case '2.0':
return 'mdi-surround-sound-2-0';
case '3.1':
return 'mdi-surround-sound-3-1';
case '5.1':
return 'mdi-surround-sound-5-1';
case '7.1':
return 'mdi-surround-sound-7-1';
default:
return 'mdi-surround-sound';
}
},
getItemBackdrop(id: string): string {
if (window.innerWidth < window.innerHeight) {
@@ -85,84 +397,9 @@ export default Vue.extend({
return `${this.$axios.defaults.baseURL}/Items/${id}/Images/Backdrop`;
}
},
getEndsAtTime(ticks: number): string {
const ms = this.ticksToMs(ticks);
const endTimeLong = new Date(Date.now() + ms);
// TODO: Respect user locale when rendering time
const endTimeShort = endTimeLong.toLocaleString('en-US', {
hour: 'numeric',
minute: 'numeric'
});
// TODO: Use a Date object
return this.$t('endsAt', {
time: endTimeShort
}).toString();
},
ticksToTime(ticks: number) {
const min = this.ticksToMs(ticks) / 60;
if (Math.floor(min / 60) && Math.floor(min % 60)) {
return `${Math.floor(min / 60)} hrs ${Math.floor(min % 60)} min`;
} else if (Math.floor(min / 60)) {
return `${Math.floor(min / 60)} hrs`;
} else {
return `${Math.floor(min % 60)} min`;
}
},
renderItemSubHeading() {
const response = [];
if (this.item.ProductionYear) {
response.push(this.item.ProductionYear);
}
if (this.item.Genres) {
response.push(this.item.Genres[0]);
}
if (this.item.RunTimeTicks) {
response.push(this.ticksToTime(this.item.RunTimeTicks));
}
if (this.item.RunTimeTicks) {
response.push(this.getEndsAtTime(this.item.RunTimeTicks));
}
return response.join(' ');
},
updateBackdropImage() {
this.backdropImageSource = this.getItemBackdrop(this.item.Id || '');
}
}
});
</script>
<style scoped>
.item-container {
margin: auto;
max-width: calc(85vh * 16 / 9);
}
.backdrop-image {
max-width: 95em;
margin: auto;
}
.item-details-container {
padding: 1em;
}
.gradient-container {
background: linear-gradient(0deg, #0c0c0c, transparent);
height: 30vh;
}
.item-sub-heading {
font-size: 0.8rem;
width: fit-content;
}
@media screen and (max-width: 30em) {
.item-overview {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 90vw;
}
}
</style>
+287
View File
@@ -0,0 +1,287 @@
<template>
<v-container>
<v-row>
<v-col cols="12">
<v-row>
<v-col cols="2">
<v-img
class="person-image elevation-2 ml-2"
cover
aspect-ratio="1"
:src="`${$axios.defaults.baseURL}/Items/${item.Id}/Images/Primary`"
/>
</v-col>
<v-col cols="7">
<div
class="text-subtitle-1 text--secondary font-weight-medium text-capitalize"
>
{{ $t('person') }}
</div>
<h1 class="text-h2 font-weight-light">{{ item.Name }}</h1>
<v-col cols="9" class="pl-0 pr-0">
<p class="item-overview">{{ item.Overview }}</p>
</v-col>
</v-col>
<v-col cols="3">
<v-row v-if="birthDate">
<v-col cols="3" class="text--secondary">Birth</v-col>
<v-col cols="9">
{{ birthDate }}
</v-col>
</v-row>
<v-row v-if="deathDate">
<v-col cols="3" class="text--secondary">Death</v-col>
<v-col cols="9">
{{ deathDate }}
</v-col>
</v-row>
<v-row v-if="birthPlace">
<v-col cols="3" class="text--secondary">Birthplace</v-col>
<v-col cols="9">
{{ birthPlace }}
</v-col>
</v-row>
<v-row v-if="ProductionLocations && ProductionLocations.length > 0">
<v-col cols="3" class="text--secondary">Birth place</v-col>
<v-col cols="9">
{{ item.ProductionLocations[0] }}
</v-col>
</v-row>
</v-col>
</v-row>
<v-row v-if="movies.length > 0">
<v-col>
<h1 class="text-h5 mb-2 ml-2 header">
<span>{{ $t('movies') }}</span>
</h1>
<vueper-slides
:bullets="false"
:bullets-outside="false"
:arrows-outside="false"
:visible-slides="6"
:slide-multiple="true"
:breakpoints="breakpoints"
fixed-height="true"
>
<vueper-slide v-for="movie in movies" :key="movie.Id">
<template v-slot:content>
<card :item="movie" />
</template>
</vueper-slide>
<template v-slot:arrow-left>
<v-btn icon large>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
</template>
<template v-slot:arrow-right>
<v-btn icon large>
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</template>
</vueper-slides>
</v-col>
</v-row>
<v-row v-if="shows.length > 0">
<v-col>
<h1 class="text-h5 mb-2 ml-2 header">
<span>{{ $t('shows') }}</span>
</h1>
<vueper-slides
:bullets="false"
:bullets-outside="false"
:arrows-outside="false"
:visible-slides="6"
:slide-multiple="true"
:breakpoints="breakpoints"
fixed-height="true"
>
<vueper-slide v-for="show in shows" :key="show.Id">
<template v-slot:content>
<card :item="show" />
</template>
</vueper-slide>
<template v-slot:arrow-left>
<v-btn icon large>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
</template>
<template v-slot:arrow-right>
<v-btn icon large>
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</template>
</vueper-slides>
</v-col>
</v-row>
</v-col>
</v-row>
</v-container>
</template>
<script lang="ts">
import Vue from 'vue';
import { BaseItemDto } from '~/api';
import imageHelper from '~/mixins/imageHelper';
import timeUtils from '~/mixins/timeUtils';
export default Vue.extend({
mixins: [imageHelper, timeUtils],
data() {
return {
item: {} as BaseItemDto,
appearances: [] as BaseItemDto[],
backdropImageSource: '',
breakpoints: {
600: {
visibleSlides: 3
},
960: {
visibleSlides: 4
},
1264: {
visibleSlides: 6
},
1904: {
visibleSlides: 6
}
}
};
},
computed: {
birthDate() {
if (this.$data.item.PremiereDate) {
return new Date(this.$data.item.PremiereDate);
} else {
return null;
}
},
deathDate: {
get() {
if (this.$data.item.EndDate) {
return new Date(this.$data.item.EndDate);
} else {
return null;
}
}
},
birthPlace: {
get() {
if (this.$data.item.ProductionLocations) {
return this.$data.item.ProductionLocations[0];
} else {
return null;
}
}
},
movies: {
get() {
return this.$data.appearances
.filter((appearance: BaseItemDto) => {
return appearance.Type === 'Movie';
})
.slice(0, 10);
}
},
shows: {
get() {
return this.$data.appearances
.filter((appearance: BaseItemDto) => {
return appearance.Type === 'Series';
})
.slice(0, 10);
}
}
},
async beforeMount() {
const item = (
await this.$api.userLibrary.getItem({
userId: this.$auth.user.Id,
itemId: this.$route.params.itemId
})
).data;
if (item) {
this.$store.dispatch('backdrop/set', { item });
this.item = item;
}
const appearances = (
await this.$api.items.getItems({
uId: this.$auth.user.Id,
userId: this.$auth.user.Id,
personIds: this.$route.params.itemId,
recursive: true,
collapseBoxSetItems: false
})
).data.Items;
if (appearances) {
this.appearances = appearances;
}
},
destroyed() {
this.$store.dispatch('backdrop/clear');
}
});
</script>
<style lang="scss" scoped>
.person-image {
border-radius: 50%;
}
.header span {
padding-left: 0.25em;
}
.header::before {
background-color: white;
content: '';
position: relative;
display: inline-block;
height: 1px;
bottom: 0.3em;
left: 0;
width: 1.25em;
}
</style>
<style>
.vueperslides__track {
position: relative;
cursor: default !important;
}
@media (hover: none) {
.vueperslides__arrows {
display: none !important;
}
}
.vueperslides__arrows {
display: flex;
position: absolute;
top: -2.75em;
right: 0;
align-items: center;
}
.vueperslides__arrow {
position: relative;
display: inline-flex;
transform: none;
}
.vueperslides__arrow--prev {
margin-right: 0.75em;
}
.vueperslides:not(.no-shadow):not(.vueperslides--3d)
.vueperslides__parallax-wrapper::after,
.vueperslides:not(.no-shadow):not(.vueperslides--3d)
.vueperslides__parallax-wrapper::before {
box-shadow: none;
}
</style>
+1
View File
@@ -21,6 +21,7 @@
"@nuxtjs/axios",
"@nuxtjs/auth-next",
"@nuxtjs/vuetify",
"@nuxtjs/date-fns",
"nuxt-i18n",
"jest"
]
+22
View File
@@ -1733,6 +1733,13 @@
consola "^2.15.0"
defu "^3.1.0"
"@nuxtjs/date-fns@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@nuxtjs/date-fns/-/date-fns-1.4.0.tgz#7cb0a03938e95bced9b089820c32982a9e0638c7"
integrity sha512-YuAKM9Y1Ig3u5ZPlW466taL4S7llE5oxgM4Vnth3pEqpRtnAF3jlxlQ3MYv5HcdOnhWBx2pgrbRTSQmCqKF/4w==
dependencies:
date-fns "^2.16.1"
"@nuxtjs/eslint-config-typescript@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@nuxtjs/eslint-config-typescript/-/eslint-config-typescript-4.0.0.tgz#0873aab4ac74cb2b9d7f7b0f8bc9449a0f57a6a5"
@@ -4627,6 +4634,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.16.1:
version "2.16.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.1.tgz#05775792c3f3331da812af253e1a935851d3834b"
integrity sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
@@ -5945,6 +5957,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.10.0:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
fontsource-noto-sans@^3.0.10:
version "3.0.10"
resolved "https://registry.yarnpkg.com/fontsource-noto-sans/-/fontsource-noto-sans-3.0.10.tgz#1fffa82a748b91c2a4c0b2d97bbd5bd7245eed8b"
integrity sha512-Gn8ucYOSk2p2Fzj9x4K6DMoX+bhk6nfwVD7h59QFr5uJL8ha2B5i3It9ZIy3AV1+qbgIY3kCmyx9pSWnpB85SQ==
for-in@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -7928,6 +7945,11 @@ known-css-properties@^0.19.0:
resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.19.0.tgz#5d92b7fa16c72d971bda9b7fe295bdf61836ee5b"
integrity sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==
langs@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/langs/-/langs-2.0.0.tgz#00c32ce48152a49a614450b9ba2632ab58a0a364"
integrity sha1-AMMs5IFSpJphRFC5uiYyq1igo2Q=
last-call-webpack-plugin@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"