feat(home): add scroller for latest items

This commit is contained in:
MrTimscampi 2020-12-03 23:10:48 +01:00
parent 24a3b02e6b
commit 7fd580d80d
8 changed files with 291 additions and 22 deletions

192
components/HomeHeader.vue Normal file
View File

@ -0,0 +1,192 @@
<template>
<swiper v-show="items.length > 0" class="swiper" :options="swiperOptions">
<swiper-slide v-for="item in items" :key="item.Id">
<div
class="slide-backdrop"
:style="{
backgroundImage: `url('${getBackdrop(item)}')`
}"
/>
<div class="slide-backdrop-overlay" />
<div class="slide-content">
<v-container class="mx-10 mt-5">
<v-row>
<v-col cols="5">
<v-img
v-if="item.ImageTags && item.ImageTags.Logo"
:src="getLogo(item)"
/>
<h1
v-else-if="item.Type === 'Episode'"
class="text-h2 text-truncate mb-2"
>
{{ item.SeriesName }}
</h1>
<h1
v-else-if="item.Type === 'MusicAlbum'"
class="text-h4 text-truncate mb-2"
>
{{ item.AlbumArtist }}
</h1>
<h1 v-else class="text-h2 text-truncate">{{ item.Name }}</h1>
<p
v-if="item.Type === 'Episode'"
class="mb-n1 text-truncate text-subtitle-2"
>
{{ item.SeasonName }}
{{ $t('episodeNumber', { episodeNumber: item.IndexNumber }) }}
</p>
<h2 v-else-if="item.Taglines" class="text-truncate">
{{ item.Taglines[0] }}
</h2>
<h2 v-if="item.Type === 'Episode'" class="text-h4 text-truncate">
{{ item.Name }}
</h2>
<h2
v-else-if="item.Type === 'MusicAlbum'"
class="text-h2 text-truncate"
>
{{ item.Name }}
</h2>
<media-info
:item="item"
year
tracks
runtime
rating
class="mt-2"
/>
<p class="mt-2" v-html="getOverview(item)" />
<v-btn
class="mr-2"
color="primary"
min-width="8em"
depressed
rounded
:to="`item/${item.Id}/play`"
>{{ $t('play') }}</v-btn
>
<v-btn
min-width="12em"
outlined
rounded
:to="`item/${item.Id}`"
>{{ $t('viewDetails') }}</v-btn
>
</v-col>
</v-row>
</v-container>
</div>
</swiper-slide>
<div slot="pagination" class="swiper-pagination"></div>
</swiper>
</template>
<script lang="ts">
import Vue from 'vue';
import { SwiperOptions } from 'swiper';
import { BaseItemDto, ImageType, ItemFields } from '~/api';
import htmlHelper from '~/mixins/htmlHelper';
import imageHelper from '~/mixins/imageHelper';
export default Vue.extend({
mixins: [htmlHelper, imageHelper],
data() {
return {
items: [] as BaseItemDto[],
swiperOptions: {
autoplay: {
delay: 20000
},
initialSlide: 1, // For some reason, slide 0 is the last slide, when loop is enabled
loop: true,
effect: 'slide',
pagination: {
el: '.swiper-pagination'
}
} as SwiperOptions
};
},
async beforeMount() {
this.items = (
await this.$api.userLibrary.getLatestMedia({
userId: this.$auth.user.Id,
limit: 10,
fields: [ItemFields.Overview],
enableImageTypes: [ImageType.Backdrop, ImageType.Logo],
imageTypeLimit: 1
})
).data;
},
methods: {
getBackdrop(item: BaseItemDto): string {
// TODO: Improve the image mixin and move this there
if (item.Type === 'Episode') {
return `${this.$axios.defaults.baseURL}/Items/${item.SeriesId}/Images/Backdrop`;
} else if (item.Type === 'MusicAlbum') {
return `${this.$axios.defaults.baseURL}/Items/${item?.AlbumArtists?.[0].Id}/Images/Backdrop`;
} else {
return `${this.$axios.defaults.baseURL}/Items/${item.Id}/Images/Backdrop`;
}
},
getLogo(item: BaseItemDto): string {
// TODO: Improve the image mixin and move this there
if (item.Type === 'Episode') {
return `${this.$axios.defaults.baseURL}/Items/${item.SeriesId}/Images/Logo`;
} else {
return `${this.$axios.defaults.baseURL}/Items/${item.Id}/Images/Logo`;
}
},
getOverview(item: BaseItemDto): string {
if (item.Overview) {
return this.sanitizeHtml(item.Overview);
} else {
return '';
}
}
}
});
</script>
<style lang="scss" scoped>
.swiper {
margin-bottom: -128px !important;
}
.slide-backdrop {
padding-bottom: 46.25%;
background-position: right center;
background-size: contain;
background-repeat: no-repeat;
box-sizing: border-box;
mask-image: linear-gradient(
180deg,
rgba(18, 18, 18, 1) 60%,
rgba(18, 18, 18, 0) 100%
),
linear-gradient(90deg, rgba(18, 18, 18, 1) 20%, rgba(18, 18, 18, 0) 70%);
mask-composite: subtract;
z-index: 1;
animation: backdrop-fadein 800ms ease-in normal both;
}
.slide-backdrop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
box-sizing: border-box;
z-index: 1;
}
.slide-content {
position: absolute;
top: 56px;
left: 0;
right: 0;
bottom: 0;
box-sizing: border-box;
z-index: 2;
}
</style>

View File

@ -66,6 +66,14 @@ export default Vue.extend({
},
async created() {
switch (this.section.type) {
case 'libraries': {
const userViewsItems = await this.$api.userViews.getUserViews({
userId: this.$auth.user.Id
});
this.items = userViewsItems.data.Items as BaseItemDto[];
break;
}
case 'resume': {
const resumeItems = await this.$api.items.getResumeItems({
userId: this.$auth.user.Id,

View File

@ -1,17 +1,15 @@
{
"actors": "Actors",
"albums": "Albums",
"alphabetically": "Alphabetically",
"artist": "Artist",
"actors": "Actors",
"albums": "Albums",
"artists": "Artists",
"badRequest": "Bad request. Try again",
"biography": "Biography",
"browserNotSupported": "Your browser is not supported for playing this file.",
"byArtist": "By",
"changeServer": "Change server",
"changeUser": "Change user",
"connect": "Connect",
"byArtist": "By",
"collections": "Collections",
"connect": "Connect",
"continueListening": "Continue listening",
@ -30,6 +28,8 @@
"home": "Home",
"incorrectUsernameOrPassword": "Incorrect username or password",
"itemNotFound": "Item not found",
"latestLibrary": "Latest {libraryName}",
"libraries": "Libraries",
"libraryEmpty": "This library is empty",
"libraryNotFound": "Library not found",
"liked": "Liked",
@ -44,8 +44,10 @@
"movies": "Movies",
"name": "Name",
"networks": "Networks",
"nextUp": "Next up",
"noNetworkConnection": "No network connection",
"noResultsFound": "No results found",
"numberTracks": "{number} tracks",
"parentalRatings": "Parental Ratings",
"password": "Password",
"play": "Play",
@ -55,8 +57,8 @@
"releaseDate": "Release date",
"resumable": "Resumable",
"selectServer": "Select server",
"series": "Series",
"selectUser": "Select a user",
"series": "Series",
"serverAddress": "Server address",
"serverAddressMustBeUrl": "Server address must be a valid URL",
"serverAddressRequired": "Server address is required",
@ -74,8 +76,8 @@
"themeSong": "Theme Song",
"themeVideo": "Theme Video",
"trailer": "Trailer",
"unexpectedError": "Unexpected error",
"unableGetRelated": "Unable to get related items",
"unexpectedError": "Unexpected error",
"unhandledException": "Unhandled exception",
"unliked": "Unliked",
"unplayed": "Unplayed",
@ -83,6 +85,7 @@
"username": "Username",
"usernameRequired": "Username is required",
"videoTypes": "Video Types",
"viewDetails": "View details",
"years": "Years",
"youMayAlsoLike": "You may also like"
}

View File

@ -61,6 +61,7 @@ const config: NuxtConfig = {
// General
'plugins/appInitPlugin.ts',
// Components
'plugins/components/swiper.ts',
'plugins/components/vueperSlides.ts',
'plugins/components/vueVirtualScroller.ts',
// Utility

View File

@ -43,7 +43,9 @@
"nuxt-vuex-localstorage": "^1.2.7",
"qs": "^6.9.4",
"shaka-player": "^3.0.6",
"swiper": "5.x",
"uuid": "^8.3.1",
"vue-awesome-swiper": "^4.1.1",
"vue-virtual-scroller": "^1.0.10",
"vueperslides": "^2.12.1"
},

View File

@ -1,12 +1,15 @@
<template>
<v-container>
<v-row
v-for="(homeSection, index) in homeSections"
:key="`homeSection-${index}`"
>
<home-section :section="homeSection" />
</v-row>
</v-container>
<div class="home-header-margin">
<home-header />
<v-container class="sections-after-header">
<v-row
v-for="(homeSection, index) in homeSections"
:key="`homeSection-${index}`"
>
<home-section :section="homeSection" />
</v-row>
</v-container>
</div>
</template>
<script lang="ts">
@ -52,10 +55,11 @@ export default Vue.extend({
if (!Object.keys(homeSectionsArray).length) {
homeSectionsArray = {
homeSection0: 'resume',
homeSection1: 'resumeaudio',
homeSection2: 'upnext',
homeSection3: 'latestmedia'
homeSection0: 'librarytiles',
homeSection1: 'resume',
homeSection2: 'resumeaudio',
homeSection3: 'upnext',
homeSection4: 'latestmedia'
};
}
@ -67,6 +71,15 @@ export default Vue.extend({
for (const homeSection of homeSectionsArray as Array<string>) {
switch (homeSection) {
case 'librarytiles': {
homeSections.push({
name: this.$t('libraries'),
libraryId: '',
shape: 'thumb-card',
type: 'libraries'
});
break;
}
case 'latestmedia': {
const latestMediaSections = [];
@ -90,7 +103,9 @@ export default Vue.extend({
}
latestMediaSections.push({
name: `Latest ${userView.Name}`,
name: this.$t('latestLibrary', {
libraryName: userView.Name
}),
libraryId: userView.Id || '',
shape: getShapeFromCollectionType(userView.CollectionType),
type: 'latestmedia'
@ -103,7 +118,7 @@ export default Vue.extend({
}
case 'resume':
homeSections.push({
name: 'Continue Watching',
name: this.$t('continueWatching'),
libraryId: '',
shape: 'thumb-card',
type: 'resume'
@ -111,7 +126,7 @@ export default Vue.extend({
break;
case 'resumeaudio':
homeSections.push({
name: 'Continue Listening',
name: this.$t('continueListening'),
libraryId: '',
shape: 'square-card',
type: 'resumeaudio'
@ -119,7 +134,7 @@ export default Vue.extend({
break;
case 'upnext':
homeSections.push({
name: 'Next Up',
name: this.$t('nextUp'),
libraryId: '',
shape: 'thumb-card',
type: 'upnext'
@ -138,3 +153,21 @@ export default Vue.extend({
}
});
</script>
<style lang="scss" scoped>
@import '~vuetify/src/styles/styles.sass';
.home-header-margin {
margin-top: -56px;
}
@media #{map-get($display-breakpoints, 'md-and-up')} {
.home-header-margin {
margin-top: -64px;
}
}
.sections-after-header {
position: relative;
z-index: 4;
}
</style>

View File

@ -0,0 +1,5 @@
import Vue from 'vue';
import VueAwesomeSwiper from 'vue-awesome-swiper';
import 'swiper/css/swiper.css';
Vue.use(VueAwesomeSwiper);

View File

@ -5001,6 +5001,13 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
dom7@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/dom7/-/dom7-2.1.5.tgz#a79411017800b31d8400070cdaebbfc92c1f6377"
integrity sha512-xnhwVgyOh3eD++/XGtH+5qBwYTgCm0aW91GFgPJ3XG+jlsRLyJivnbP0QmUBFhI+Oaz9FV0s7cxgXHezwOEBYA==
dependencies:
ssr-window "^2.0.0"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@ -11638,6 +11645,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
ssr-window@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-2.0.0.tgz#98c301aef99523317f8d69618f0010791096efc4"
integrity sha512-NXzN+/HPObKAx191H3zKlYomE5WrVIkoCB5IaSdvKokxTpjBdWfr0RaP+1Z5KOfDT0ZVz+2tdtiBkhsEQ9p+0A==
ssri@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
@ -12053,6 +12065,14 @@ svgo@^1.0.0:
unquote "~1.1.1"
util.promisify "~1.0.0"
swiper@5.x:
version "5.4.5"
resolved "https://registry.yarnpkg.com/swiper/-/swiper-5.4.5.tgz#a350f654bf68426dbb651793824925512d223c0f"
integrity sha512-7QjA0XpdOmiMoClfaZ2lYN6ICHcMm72LXiY+NF4fQLFidigameaofvpjEEiTQuw3xm5eksG5hzkaRsjQX57vtA==
dependencies:
dom7 "^2.1.5"
ssr-window "^2.0.0"
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -12785,6 +12805,11 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
vue-awesome-swiper@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/vue-awesome-swiper/-/vue-awesome-swiper-4.1.1.tgz#8f7ab221ad003021d756b86aa618f429924900fe"
integrity sha512-50um10t6N+lJaORkpwSi1wWuMmBI1sgFc9Znsi5oUykw2cO5DzLaBHcO2JNX21R+Ue4TGoIJDhhxjBHtkFrTEQ==
vue-client-only@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/vue-client-only/-/vue-client-only-2.0.0.tgz#ddad8d675ee02c761a14229f0e440e219de1da1c"