Merge pull request #48 from KevinMidboe/refactor/image-loading

Refactor/image loading
This commit is contained in:
2020-02-20 00:22:14 +01:00
committed by GitHub
2 changed files with 320 additions and 185 deletions

View File

@@ -2,28 +2,32 @@
<section class="movie"> <section class="movie">
<!-- HEADER w/ POSTER --> <!-- HEADER w/ POSTER -->
<header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }" :class="compact ? 'compact' : ''" @click="compact=!compact"> <header ref="header" :class="compact ? 'compact' : ''" @click="compact=!compact">
<div class="movie__wrap movie__wrap--header">
<figure class="movie__poster"> <figure class="movie__poster">
<img class="movie-item__img is-loaded"
ref="poster-image"
src="~assets/placeholder.png">
<!-- -->
<!-- <img v-else class="movie-item__img is-loaded" src="~assets/no-image.png" ref="image" alt="No image - linked image unavailable"> -->
</figure>
<!-- <figure class="movie__poster">
<img v-if="movie && poster === null" <img v-if="movie && poster === null"
class="movies-item__img is-loaded" class="movies-item__img is-loaded"
alt="movie poster image" alt="movie poster image"
src="~assets/no-image.png"> src="~assets/no-image.png">
<img v-else-if="poster === undefined" <img v-else-if="poster === undefined"
class="movies-item__img grey" class="movies-item__img grey"
alt="movie poster image"> alt="movie poster image"
<!-- src="~assets/placeholder.png"> --> src="~assets/placeholder.png">
<img v-else <img v-else
class="movies-item__img is-loaded" class="movies-item__img is-loaded"
alt="movie poster image" alt="movie poster image"
:src="ASSET_URL + ASSET_SIZES[0] + poster"> :src="ASSET_URL + ASSET_SIZES[0] + poster">
</figure> </figure> -->
<div class="movie__title"> <h1 class="movie__title" v-if="movie">{{ movie.title }}</h1>
<h1 v-if="movie">{{ movie.title }}</h1>
<loading-placeholder v-else :count="1" /> <loading-placeholder v-else :count="1" />
</div>
</div>
</header> </header>
<!-- Siderbar and movie info --> <!-- Siderbar and movie info -->
@@ -64,32 +68,48 @@
<!-- MOVIE INFO --> <!-- MOVIE INFO -->
<div class="movie__info"> <div class="movie__info">
<div class="movie__description" v-if="movie"> {{ movie.overview }}</div>
<!-- Loading placeholder --> <!-- Loading placeholder -->
<div class="movie__description noselect"
@click="truncatedDescription=!truncatedDescription"
v-if="!loading">
<span :class="truncatedDescription ? 'truncated':null">{{ movie.overview }}</span>
<button class="truncate-toggle"><i></i></button>
</div>
<div v-else class="movie__description"> <div v-else class="movie__description">
<loading-placeholder :count="12" /> <loading-placeholder :count="5" />
</div> </div>
<div class="movie__details" v-if="movie"> <div class="movie__details" v-if="movie">
<div v-if="movie.year" class="movie__details-block"> <div v-if="movie.year">
<h2 class="movie__details-title">Release Date</h2> <h2 class="title">Release Date</h2>
<div class="movie__details-text">{{ movie.year }}</div> <div class="text">{{ movie.year }}</div>
</div> </div>
<div v-if="movie.rank" class="movie__details-block"> <div v-if="movie.rating">
<h2 class="movie__details-title">Rating</h2> <h2 class="title">Rating</h2>
<div class="movie__details-text">{{ movie.rank }}</div> <div class="text">{{ movie.rating }}</div>
</div> </div>
<div v-if="movie.type == 'show'" class="movie__details-block"> <div v-if="movie.type == 'show'">
<h2 class="movie__details-title">Seasons</h2> <h2 class="title">Seasons</h2>
<div class="movie__details-text">{{ movie.seasons }}</div> <div class="text">{{ movie.seasons }}</div>
</div> </div>
<div v-if="movie.genres" class="movie__details-block"> <div v-if="movie.genres">
<h2 class="movie__details-title">Genres</h2> <h2 class="title">Genres</h2>
<div class="movie__details-text">{{ nestedDataToString(movie.genres) }}</div> <div class="text">{{ movie.genres.join(', ') }}</div>
</div>
<div v-if="movie.type == 'show'">
<h2 class="title">Production status</h2>
<div class="text">{{ movie.production_status }}</div>
</div>
<div v-if="movie.type == 'show'">
<h2 class="title">Runtime</h2>
<div class="text">{{ movie.runtime[0] }} minutes</div>
</div> </div>
</div> </div>
@@ -126,7 +146,17 @@ import LoadingPlaceholder from './ui/LoadingPlaceholder'
import { getMovie, getPerson, getShow, request, getRequestStatus } from '@/api' import { getMovie, getPerson, getShow, request, getRequestStatus } from '@/api'
export default { export default {
props: ['id', 'type'], // props: ['id', 'type'],
props: {
id: {
required: true,
type: Number
},
type: {
required: false,
type: String
}
},
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement }, components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
directives: { img: img }, // TODO decide to remove or use directives: { img: img }, // TODO decide to remove or use
data(){ data(){
@@ -142,11 +172,40 @@ export default {
requested: false, requested: false,
admin: localStorage.getItem('admin') == "true" ? true : false, admin: localStorage.getItem('admin') == "true" ? true : false,
showTorrents: false, showTorrents: false,
compact: false compact: false,
loading: true,
truncatedDescription: true
}
},
watch: {
id: function(val){
if (this.type === 'movie') {
this.fetchMovie(val);
} else {
this.fetchShow(val)
}
},
backdrop: function(backdrop) {
if (backdrop != null) {
const style = {
backgroundImage: 'url(' + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ')'
}
Object.assign(this.$refs.header.style, style)
}
}
},
computed: {
numberOfTorrentResults: () => {
let numTorrents = store.getters['torrentModule/resultCount']
return numTorrents !== null ? numTorrents + ' results' : null
} }
}, },
methods: { methods: {
parseResponse(movie) { parseResponse(movie) {
setTimeout(() => {
this.loading = false
this.movie = { ...movie } this.movie = { ...movie }
this.title = movie.title this.title = movie.title
this.poster = movie.poster this.poster = movie.poster
@@ -155,13 +214,22 @@ export default {
this.checkIfRequested(movie) this.checkIfRequested(movie)
.then(status => this.requested = status) .then(status => this.requested = status)
store.dispatch('documentTitle/updateTitle', movie.title) store.dispatch('documentTitle/updateTitle', movie.title)
this.setPosterSrc()
}, 1000)
}, },
async checkIfRequested(movie) { async checkIfRequested(movie) {
return await getRequestStatus(movie.id, movie.type) return await getRequestStatus(movie.id, movie.type)
}, },
nestedDataToString(data) { setPosterSrc() {
return data.join(', ') const poster = this.$refs['poster-image']
if (this.poster == null) {
poster.src = '/dist/no-image.png'
return
}
poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`
}, },
sendRequest(){ sendRequest(){
request(this.id, this.type, storage.token) request(this.id, this.type, storage.token)
@@ -176,24 +244,6 @@ export default {
window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id
}, },
}, },
watch: {
id: function(val){
if (this.type === 'movie') {
this.fetchMovie(val);
} else {
this.fetchShow(val)
}
}
},
computed: {
numberOfTorrentResults: () => {
let numTorrents = store.getters['torrentModule/resultCount']
return numTorrents !== null ? numTorrents + ' results' : null
}
},
beforeDestroy() {
store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle)
},
created() { created() {
this.prevDocumentTitle = store.getters['documentTitle/title'] this.prevDocumentTitle = store.getters['documentTitle/title']
@@ -216,8 +266,9 @@ export default {
this.$router.push({ name: '404' }); this.$router.push({ name: '404' });
}) })
} }
},
console.log('admin: ', this.admin) beforeDestroy() {
store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle)
} }
} }
</script> </script>
@@ -226,6 +277,89 @@ export default {
@import "./src/scss/loading-placeholder"; @import "./src/scss/loading-placeholder";
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@import "./src/scss/main";
header {
$duration: 0.2s;
height: 250px;
transform: scaleY(1);
transition: height $duration ease;
transform-origin: top;
position: relative;
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
background-color: $background-color;
display: flex;
align-items: center;
@include tablet-min {
height: 350px;
}
&:before {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
background: $background-dark-85;
}
@include mobile {
&.compact {
height: 100px;
}
}
}
.movie__poster {
display: none;
@include desktop {
background: $background-color;
height: 0;
display: block;
position: absolute;
width: calc(45% - 40px);
top: 40px;
left: 40px;
> img {
width: 100%;
}
}
}
.truncate-toggle {
border: none;
background: none;
width: 100%;
display: flex;
align-items: center;
text-align: center;
color: $text-color;
> i {
font-style: unset;
font-size: 0.7rem;
transition: 0.3s ease all;
transform: rotateY(180deg)
}
&::before, &::after {
content: '';
flex: 1;
border-bottom: 1px solid $text-color-50;
}
&::before {
margin-right: 1rem;
}
&::after {
margin-left: 1rem;
}
}
.movie { .movie {
&__wrap { &__wrap {
@@ -246,49 +380,6 @@ export default {
color: $text-color; color: $text-color;
} }
} }
&__header {
$duration: 0.2s;
height: 250px;
transform: scaleY(1);
transition: height $duration ease;
transform-origin: top;
position: relative;
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
background-color: $background-color;
@include tablet-min {
height: 350px;
}
&:before {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
background: $background-dark-85;
}
&.compact {
height: 100px;
}
}
&__poster {
display: none;
@include tablet-min {
background: $background-color;
height: 0;
display: block;
position: absolute;
width: calc(45% - 40px);
top: 40px;
left: 40px;
}
}
&__img { &__img {
display: block; display: block;
@@ -364,24 +455,35 @@ export default {
font-size: 13px; font-size: 13px;
line-height: 1.8; line-height: 1.8;
margin-bottom: 20px; margin-bottom: 20px;
& .truncated {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
& + .truncate-toggle > i {
transform: rotateY(0deg) rotateZ(180deg);
}
}
@include tablet-min { @include tablet-min {
margin-bottom: 30px; margin-bottom: 30px;
font-size: 14px; font-size: 14px;
} }
} }
&__details { &__details {
&-block { display: flex;
float: left; flex-wrap: wrap;
}
&-block:not(:last-child) { > div {
margin-bottom: 20px; margin-bottom: 20px;
margin-right: 20px; margin-right: 20px;
@include tablet-min { @include tablet-min {
margin-bottom: 30px; margin-bottom: 30px;
margin-right: 30px; margin-right: 30px;
} }
} & .title {
&-title {
margin: 0; margin: 0;
font-weight: 400; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
@@ -391,12 +493,13 @@ export default {
font-size: 16px; font-size: 16px;
} }
} }
&-text { & .text {
font-weight: 300; font-weight: 300;
font-size: 14px; font-size: 14px;
margin-top: 5px; margin-top: 5px;
} }
} }
}
&__admin { &__admin {
width: 100%; width: 100%;
padding: 20px; padding: 20px;

View File

@@ -1,21 +1,24 @@
<template> <template>
<li class="movies-item" :class="{'shortList': shortList}"> <li class="movie-item" :class="{'shortList': shortList}">
<a class="movies-item__link" :class="{'no-image': !movie}" @click.prevent="openMoviePopup(movie.id, movie.type)"> <figure class="movie-item__poster">
<img class="movie-item__img is-loaded"
<!-- TODO change to picture element --> ref="poster-image"
<figure class="movies-item__poster"> @click="openMoviePopup(movie.id, movie.type)"
<img v-if="movie.poster" class="movies-item__img is-loaded" ref="image" src="~assets/placeholder.png" :alt="`${movie.title} poster image`"> :alt="posterAltText"
:data-src="poster"
src="~assets/placeholder.png">
<div v-if="movie.download" class="progress"> <div v-if="movie.download" class="progress">
<progress :value="movie.download.progress" max="100"></progress> <progress :value="movie.download.progress" max="100"></progress>
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span> <span>{{ movie.download.state }}: {{ movie.download.progress }}%</span>
</div> </div>
</figure> </figure>
<div class="movies-item__content">
<p class="movies-item__title">{{ movie.title || movie.name }}</p> <div class="movie-item__info">
<p class="movies-item__title">{{ movie.year }}</p> <p v-if="movie.title || movie.name">{{ movie.title || movie.name }}</p>
<p v-if="movie.year">{{ movie.year }}</p>
<p v-if="movie.type == 'person'">Known for: {{ movie.known_for_department }}</p>
</div> </div>
</a>
</li> </li>
</template> </template>
@@ -38,6 +41,8 @@ export default {
}, },
data(){ data(){
return { return {
poster: undefined,
observed: false,
posterSizes: [{ posterSizes: [{
id: 'w500', id: 'w500',
minWidth: 500 minWidth: 500
@@ -54,37 +59,35 @@ export default {
} }
}, },
computed: { computed: {
posterUrl: function() { posterAltText: function() {
if (this.movie.poster == null) const type = this.movie.type || ''
return "~assets/no-image.png" const title = this.movie.title || this.movie.name
return this.movie.poster ? `Poster for ${type} ${title}` : `Missing image for ${type} ${title}`
const correctWidth = this.posterQualityIdentifierFromPosterWidth }
return `https://image.tmdb.org/t/p/${correctWidth}${this.movie.poster}`
}, },
posterQualityIdentifierFromPosterWidth: function() { beforeMount() {
const posterWidth = this.$refs.image.clientHeight if (this.movie.poster != null) {
if (posterWidth > this.posterSizes[0].minWidth) this.poster = 'https://image.tmdb.org/t/p/w500' + this.movie.poster
return this.posterSizes[0].id } else {
this.poster = '/dist/no-image.png'
const widthCandidates = this.posterSizes.filter(size => posterWidth < size.minWidth ? size.id : null)
return widthCandidates[widthCandidates.length - 1].id
} }
}, },
mounted() { mounted() {
if (this.$refs.image == undefined) const poster = this.$refs['poster-image']
if (poster == null)
return return
const imageObserver = new IntersectionObserver((entries, imgObserver) => { const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting && this.observed == false) {
const lazyImage = entry.target const lazyImage = entry.target
lazyImage.src = this.posterUrl lazyImage.src = lazyImage.dataset.src
lazyImage.class this.observed = true
} }
}) })
}); });
imageObserver.observe(this.$refs.image); imageObserver.observe(poster);
}, },
methods: { methods: {
openMoviePopup(id, type) { openMoviePopup(id, type) {
@@ -94,11 +97,12 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@import "./src/scss/main";
.movies-item { .movie-item {
padding: 10px; padding: 10px;
width: 50%; width: 50%;
background-color: $background-color; background-color: $background-color;
@@ -106,6 +110,7 @@ export default {
@include tablet-min { @include tablet-min {
padding: 15px; padding: 15px;
width: 33%;
} }
@include tablet-landscape-min { @include tablet-landscape-min {
padding: 15px; padding: 15px;
@@ -121,19 +126,16 @@ export default {
width: 12.5%; width: 12.5%;
} }
&__link{ &:hover &__info > p {
color: $text-color;
}
&__poster {
text-decoration: none; text-decoration: none;
color: $text-color-70; color: $text-color-70;
font-weight: 300; font-weight: 300;
}
&__content{ > img {
padding-top: 15px;
}
&__poster{
transition: transform 0.5s ease, box-shadow 0.3s ease;
transform: translateZ(0);
}
&__img{
width: 100%; width: 100%;
opacity: 0; opacity: 0;
transform: scale(0.97) translateZ(0); transform: scale(0.97) translateZ(0);
@@ -142,12 +144,20 @@ export default {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
}
&__link:not(.no-image):hover &__poster{ &:hover {
transform: scale(1.03); transform: scale(1.03);
box-shadow: 0 0 10px rgba($dark, 0.1); box-shadow: 0 0 10px rgba($dark, 0.1);
} }
&__title{ }
}
&__info {
padding-top: 15px;
font-weight: 300;
> p {
color: $text-color-70;
margin: 0; margin: 0;
font-size: 11px; font-size: 11px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
@@ -160,8 +170,30 @@ export default {
font-size: 14px; font-size: 14px;
} }
} }
&__link:hover &__title{ }
color: $text-color; }
.no-image {
background-color: var(--text-color);
color: var(--background-color);
width: 100%;
height: 383px;
display: flex;
align-items: center;
justify-content: center;
span {
font-size: 1.5rem;
width: 70%;
text-align: center;
text-transform: uppercase;
}
&:hover {
transform: scale(1);
} }
} }
</style> </style>