mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-10-07 11:33:40 +00:00
feat(home): add scroller for latest items
This commit is contained in:
parent
24a3b02e6b
commit
7fd580d80d
192
components/HomeHeader.vue
Normal file
192
components/HomeHeader.vue
Normal 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>
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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>
|
||||
|
5
plugins/components/swiper.ts
Normal file
5
plugins/components/swiper.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Vue from 'vue';
|
||||
import VueAwesomeSwiper from 'vue-awesome-swiper';
|
||||
import 'swiper/css/swiper.css';
|
||||
|
||||
Vue.use(VueAwesomeSwiper);
|
25
yarn.lock
25
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user