Tried simplifying and spliting some of Movie component.

Simplified sidebar element to use props.
Replaced icons with feather icons.

Description gets it's own component & tries it best at figuring out if
description should be truncated or not. Now it adds a element at bottom
of body with the same description and compares the height to default
truncated text. If the dummy element is taller we show the truncate
button.
This commit is contained in:
2022-01-14 16:43:45 +01:00
parent acfa3e9d54
commit 5431b5be40
12 changed files with 442 additions and 293 deletions

View File

@@ -23,41 +23,54 @@
<div class="movie__wrap movie__wrap--main">
<!-- SIDEBAR ACTIONS -->
<div class="movie__actions" v-if="movie">
<sidebar-list-element
:iconRef="'#iconNot_exsits'"
:active="matched"
:iconRefActive="'#iconExists'"
:textActive="'Already in plex 🎉'"
>
Not yet in plex
<sidebar-list-element :active="matched" :disabled="true">
<IconThumbsUp v-if="matched" />
<IconThumbsDown v-else class="stroke" />
{{ !matched ? "Not yet in plex" : "Already in plex 🎉" }}
</sidebar-list-element>
<sidebar-list-element
@click="sendRequest"
:iconRef="'#iconSent'"
:active="requested"
:textActive="'Requested to be downloaded'"
>
Request to be downloaded?
<sidebar-list-element @click="sendRequest" :active="requested">
<IconMail />
{{
!requested
? "Request to be downloaded?"
: "Requested to be downloaded"
}}
</sidebar-list-element>
<sidebar-list-element
v-if="isPlexAuthenticated && matched"
v-if="plexId && matched"
@click="openInPlex"
:iconString="'⏯ '"
>
Watch in plex now!
</sidebar-list-element>
<sidebar-list-element
v-if="
movie &&
movie.credits &&
movie.credits.cast &&
movie.credits.cast.length
"
:active="showCast"
@click="() => (showCast = !showCast)"
>
<IconProfile class="icon stroke" />
{{ showCast ? "Hide cast" : "Show cast" }}
</sidebar-list-element>
<sidebar-list-element
v-if="admin"
@click="showTorrents = !showTorrents"
:iconRef="'#icon_torrents'"
:active="showTorrents"
:supplementaryText="numberOfTorrentResults"
>
<IconMagnet class="rotate" />
Search for torrents
<span class="meta">{{ numberOfTorrentResults || 123 }}</span>
</sidebar-list-element>
<sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'">
<sidebar-list-element @click="openTmdb">
<IconInfo />
See more info
</sidebar-list-element>
</div>
@@ -78,55 +91,43 @@
<!-- MOVIE INFO -->
<div class="movie__info">
<!-- Loading placeholder -->
<div
class="movie__description noselect"
@click="truncatedDescription = !truncatedDescription"
v-if="!loading"
>
<span :class="truncatedDescription ? 'truncated' : null">{{
movie.overview
}}</span>
<button
v-if="movie.overview && movie.overview.length > 220"
class="truncate-toggle"
>
<i></i>
</button>
</div>
<div v-else class="movie__description">
<div v-if="loading">
<loading-placeholder :count="5" />
</div>
<MovieDescription v-else :description="movie.overview" />
<div class="movie__details" v-if="movie">
<div v-if="movie.year">
<h2 class="title">Release Date</h2>
<span class="info">{{ movie.year }}</span>
</div>
<div v-if="movie.rating">
<h2 class="title">Rating</h2>
<span class="info">{{ movie.rating }}</span>
</div>
<div v-if="movie.type == 'show'">
<h2 class="title">Seasons</h2>
<span class="info">{{ movie.seasons }}</span>
</div>
<div v-if="movie.genres">
<h2 class="title">Genres</h2>
<span class="info">{{ movie.genres.join(", ") }}</span>
</div>
<div v-if="movie.type == 'show'">
<h2 class="title">Production status</h2>
<span class="info">{{ movie.production_status }}</span>
</div>
<div v-if="movie.type == 'show'">
<h2 class="title">Runtime</h2>
<span class="info">{{ movie.runtime[0] }} minutes</span>
</div>
<MovieDetail
v-if="movie.year"
title="Release date"
:detail="movie.year"
/>
<MovieDetail
v-if="movie.rating"
title="Rating"
:detail="movie.rating"
/>
<MovieDetail
v-if="movie.type == 'show'"
title="Seasons"
:detail="movie.seasons"
/>
<MovieDetail
v-if="movie.genres"
title="Genres"
:detail="movie.genres.join(', ')"
/>
<MovieDetail
v-if="movie.type == 'show'"
title="Production status"
:detail="movie.production_status"
/>
<MovieDetail
v-if="movie.type == 'show'"
title="Runtime"
:detail="movie.runtime[0]"
/>
</div>
</div>
@@ -135,14 +136,16 @@
<div
class="movie__admin"
v-if="
showCast &&
movie &&
movie.credits &&
movie.credits.cast &&
movie.credits.cast.length
"
>
<h2 class="title">Cast</h2>
<Cast :cast="movie.credits.cast" />
<MovieDetail title="cast">
<Cast :cast="movie.credits.cast" />
</MovieDetail>
</div>
</div>
@@ -161,9 +164,17 @@
<script>
import { mapGetters } from "vuex";
import img from "@/directives/v-image";
import IconProfile from "../icons/IconProfile";
import IconThumbsUp from "../icons/IconThumbsUp";
import IconThumbsDown from "../icons/IconThumbsDown";
import IconInfo from "../icons/IconInfo";
import IconMail from "../icons/IconMail";
import IconMagnet from "../icons/IconMagnet";
import TorrentList from "./TorrentList";
import Cast from "./Cast";
import MovieDetail from "./ui/MovieDetail";
import SidebarListElement from "./ui/sidebarListElem";
import MovieDescription from "./ui/MovieDescription";
import store from "@/store";
import LoadingPlaceholder from "./ui/LoadingPlaceholder";
@@ -188,7 +199,20 @@ export default {
type: String
}
},
components: { TorrentList, Cast, LoadingPlaceholder, SidebarListElement },
components: {
MovieDescription,
MovieDetail,
IconProfile,
IconThumbsUp,
IconThumbsDown,
IconMail,
IconInfo,
IconMagnet,
TorrentList,
Cast,
LoadingPlaceholder,
SidebarListElement
},
directives: { img: img }, // TODO decide to remove or use
data() {
return {
@@ -201,9 +225,9 @@ export default {
matched: false,
requested: false,
showTorrents: false,
showCast: false,
compact: false,
loading: true,
truncatedDescription: true
loading: true
};
},
watch: {
@@ -226,13 +250,10 @@ export default {
}
},
computed: {
...mapGetters("user", ["loggedIn", "admin"]),
...mapGetters("user", ["loggedIn", "admin", "plexId"]),
numberOfTorrentResults: () => {
let numTorrents = store.getters["torrentModule/resultCount"];
return numTorrents !== null ? numTorrents + " results" : null;
},
isPlexAuthenticated: () => {
return store.getters["userModule/isPlexAuthenticated"];
}
},
methods: {
@@ -282,19 +303,19 @@ export default {
this.prevDocumentTitle = store.getters["documentTitle/title"];
if (this.type === "movie") {
getMovie(this.id, true)
getMovie(this.id, false, true)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: "404" });
});
} else if (this.type == "person") {
getPerson(this.id, true)
getPerson(this.id, false)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: "404" });
});
} else {
getShow(this.id, true)
getShow(this.id, false)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: "404" });
@@ -366,51 +387,6 @@ header {
}
}
.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;
}
}
h2.title {
margin: 0;
font-weight: 400;
text-transform: uppercase;
font-size: 1.2rem;
color: $green;
}
span.info {
font-weight: 300;
font-size: 1rem;
letter-spacing: 0.8px;
margin-top: 5px;
}
.movie {
&__wrap {
display: flex;
@@ -493,40 +469,9 @@ span.info {
&__info {
margin-left: 0;
}
&__description {
font-weight: 300;
font-size: 13px;
line-height: 1.8;
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 {
margin-bottom: 30px;
font-size: 14px;
}
}
&__details {
display: flex;
flex-wrap: wrap;
> div {
margin-bottom: 20px;
margin-right: 20px;
@include tablet-min {
margin-bottom: 30px;
margin-right: 30px;
}
}
}
&__admin {
width: 100%;

View File

@@ -0,0 +1,123 @@
<template>
<div
id="description"
class="movie-description noselect"
@click="overflow ? (truncated = !truncated) : null"
>
<span ref="description" :class="{ truncated }">{{ description }}</span>
<button v-if="description && overflow" class="truncate-toggle">
<IconArrowDown :class="{ rotate: !truncated }" />
</button>
</div>
</template>
<script>
import IconArrowDown from "../../icons/IconArrowDown";
export default {
components: { IconArrowDown },
props: {
description: {
type: String,
required: true
}
},
data() {
return {
truncated: true,
overflow: false
};
},
mounted() {
this.checkDescriptionOverflowing();
},
methods: {
checkDescriptionOverflowing() {
const descriptionEl = document.getElementById("description");
if (!descriptionEl) return;
const { height, width } = descriptionEl.getBoundingClientRect();
const { fontSize, lineHeight } = getComputedStyle(descriptionEl);
const elementWithoutOverflow = document.createElement("div");
elementWithoutOverflow.setAttribute(
"style",
`max-width: ${width}px; display: block; font-size: ${fontSize}; line-height: ${lineHeight};`
);
elementWithoutOverflow.classList.add("dummy-non-overflow");
elementWithoutOverflow.innerText = this.description;
document.body.appendChild(elementWithoutOverflow);
const elemWithoutOverflowHeight =
elementWithoutOverflow.getBoundingClientRect()["height"];
this.overflow = elemWithoutOverflowHeight > height;
this.removeElements(document.querySelectorAll(".dummy-non-overflow"));
},
removeElements: elems => elems.forEach(el => el.remove())
}
};
</script>
<style lang="scss" scoped>
@import "./src/scss/media-queries";
.movie-description {
font-weight: 300;
font-size: 13px;
line-height: 1.8;
margin-bottom: 20px;
transition: all 1s ease;
@include tablet-min {
margin-bottom: 30px;
font-size: 14px;
}
}
span.truncated {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
.truncate-toggle {
border: none;
background: none;
width: 100%;
display: flex;
align-items: center;
text-align: center;
color: var(--text-color);
margin-top: 1rem;
cursor: pointer;
svg {
transition: 0.4s ease all;
@include mobile {
height: 20px;
width: 20px;
}
&.rotate {
transform: rotateX(180deg);
}
}
&::before,
&::after {
content: "";
flex: 1;
border-bottom: 1px solid var(--text-color-50);
}
&::before {
margin-right: 1rem;
}
&::after {
margin-left: 1rem;
}
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="movie-detail">
<h2 class="title">{{ title }}</h2>
<span v-if="detail" class="info">{{ detail }}</span>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true
},
detail: {
required: false,
default: null
}
}
};
</script>
<style lang="scss" scoped>
@import "./src/scss/media-queries";
.movie-detail {
margin-bottom: 20px;
margin-right: 20px;
@include tablet-min {
margin-bottom: 30px;
margin-right: 30px;
}
h2.title {
margin: 0;
font-weight: 400;
text-transform: uppercase;
font-size: 1.2rem;
color: var(--color-green);
}
span.info {
font-weight: 300;
font-size: 1rem;
letter-spacing: 0.8px;
margin-top: 5px;
}
}
</style>

View File

@@ -1,76 +1,25 @@
<template>
<div>
<a @click="$emit('click')">
<li>
<figure v-if="iconRef" :class="activeClassIfActive">
<svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg>
</figure>
<span class="text" :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
<span v-if="supplementaryText" class="supplementary-text">
{{ supplementaryText }}
</span>
</li>
</a>
</div>
<li @click="event => $emit('click', event)" :class="{ active, disabled }">
<slot></slot>
</li>
</template>
<script>
// TODO if a image is hovered and we can't set the hover color we want to
// go into it and change the fill
export default {
props: {
iconRef: {
type: String,
required: false
},
iconRefActive: {
type: String,
required: false
},
active: {
type: Boolean,
default: false,
default: false
},
textActive: {
type: String,
required: false
},
supplementaryText: {
type: String,
required: false
}
},
computed: {
iconRefNameIfActive() {
const { iconRefActive, iconRef, active } = this
if ((iconRefActive && iconRef) && active) {
return iconRefActive
}
return iconRef
},
contentTextToDisplay() {
const { textActive, active, $slots } = this
if (textActive && active)
return textActive
if ($slots.default && $slots.default.length > 0)
return $slots.default[0].text
return ''
},
activeClassIfActive() {
return this.active ? 'active' : ''
disabled: {
type: Boolean,
default: false
}
}
}
};
</script>
<style lang="scss" scoped>
@import "./src/scss/variables";
@import "./src/scss/media-queries";
li {
@@ -78,61 +27,58 @@ li {
align-items: center;
text-decoration: none;
text-transform: uppercase;
color: $text-color-50;
transition: color 0.5s ease;
font-size: 11px;
color: var(--text-color-50);
font-size: 12px;
padding: 10px 0;
border-bottom: 1px solid $text-color-5;
border-bottom: 1px solid var(--text-color-5);
cursor: pointer;
&:hover {
color: $text-color;
cursor: pointer;
svg {
margin-right: 1rem;
transition: all 0.5s ease;
stroke: var(--text-color-70);
.icon {
fill: $text-color;
cursor: pointer;
transform: scale(1.1, 1.1);
&[data-fill] {
stroke: none;
fill: vaR(--text-color-70);
}
}
.active {
color: $text-color;
.icon {
fill: $green;
&:hover,
&.active {
color: var(--text-color);
svg {
stroke: var(--text-color);
transform: scale(1.1, 1.1);
&[data-fill] {
stroke: none;
fill: var(--text-color);
}
}
}
&.active svg {
stroke: var(--color-green);
&[data-fill] {
stroke: none;
fill: var(--color-green);
}
}
&.disabled {
cursor: default;
}
.pending {
color: #f8bd2d;
}
.text {
margin-left: 26px;
}
.supplementary-text {
flex-grow: 1;
.meta {
margin-left: auto;
text-align: right;
}
figure {
position: absolute;
> svg {
position: relative;
top: 50%;
width: 16px;
height: 16px;
margin: 0 7px 0 0;
fill: $text-color-50;
transition: fill 0.5s ease, transform 0.5s ease;
& .waiting {
transform: scale(0.8, 0.8);
}
& .pending {
fill: #f8bd2d;
}
}
}
}
</style>