|
<template>
|
|
<transition appear name="fade">
|
|
<div v-if="tmdbId !== undefined" class="modal p-8 overflow-auto items-center z-50" @click.self="close">
|
|
<transition name="slide" appear
|
|
@before-enter="lockScroll(true)" @enter-cancelled="lockScroll(false)"
|
|
@leave="lockScroll(false)" @leave-cancelled="lockScroll(true)">
|
|
<div v-if="movie !== undefined" class="relative w-full max-w-6xl bg-white md:bg-none overflow-hidden rounded-xl flex-col flex-shrink-0 shadow-xl">
|
|
<div class="bg-center bg-cover bg-no-repeat" :class="{ 'text-white': isDark }"
|
|
:style="movie.backdropPath ? `background: url('/api/tmdb/image/w1280${movie.backdropPath}')` : ''">
|
|
<div class="movie-modal-content z-10" :style="backdropOverlayStyle">
|
|
<div class="flex p-4 items-center">
|
|
<movie-poster v-if="movie.posterPath" :src="movie.posterPath"
|
|
class="w-3/12 flex-shrink-0 rounded-xl shadow-md md:shadow-xl" @onLoad="onPosterLoad"/>
|
|
<div class="p-4 flex flex-col justify-center">
|
|
<h2 class="text-lg md:text-4xl">
|
|
<span class="font-bold">{{ movie.title }}</span>
|
|
<span class="opacity-70" v-if="releaseYear"> ({{ releaseYear }})</span>
|
|
</h2>
|
|
<div class="flex font-light dot-separated">
|
|
<span v-if="releaseDate">{{ releaseDate }}</span>
|
|
<span v-if="runtime">{{ runtime }}</span>
|
|
</div>
|
|
<div v-if="movie?.overview" class="mt-4 hidden md:block">
|
|
<p class="">{{movie?.overview}}</p>
|
|
</div>
|
|
<div class="mt-4 hidden md:block">
|
|
<a class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
|
|
v-if="movie.plexLink !== null" :href="movie.plexLink" target="_blank">Watch Now</a>
|
|
<template v-else-if="movie.requestedBy !== null">
|
|
<button class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
|
|
v-if="movie.requestedBy.id == userId" @click="cancel">Cancel Request</button>
|
|
<button class="inline-block py-2 px-6 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
|
|
v-else disabled>Requested by Another User</button>
|
|
</template>
|
|
<button v-else class="inline-block py-2 w-36 rounded-full shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 transition-transform transform hover:scale-105" :class="{ 'bg-white text-black': isDark, 'bg-black text-white': !isDark }"
|
|
:disabled="isRequesting" @click="request">{{isRequesting ? "Processing..." : "Request"}}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 space-y-4 md:hidden z-20" style="box-shadow: 0px -10px 10px rgba(0, 0, 0, 0.10);" >
|
|
<div v-if="movie?.overview">
|
|
<h3 class="font-bold mb-2">Overview</h3>
|
|
<p>{{ movie.overview }}</p>
|
|
</div>
|
|
<div class="text-center">
|
|
<a class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white focus:outline-none ring-0"
|
|
v-if="movie.plexLink !== null" :href="movie.plexLink">Watch Now</a>
|
|
<template v-if="movie.requestedBy !== null">
|
|
<button class="inline-block py-2 px-8 rounded-full bg-red-500 text-white disabled:opacity-50" @click="cancel"
|
|
v-if="movie.requestedBy.id == userId">Cancel Request</button>
|
|
<button class="inline-block py-2 px-8 rounded-full bg-red-500 text-white disabled:opacity-50" disabled
|
|
v-else>Requested by Another User</button>
|
|
</template>
|
|
<button v-else class="inline-block py-2 px-8 rounded-full shadow-sm bg-red-500 hover:bg-black transition-colors text-white disabled:opacity-50 disabled:cursor-default focus:outline-none ring-0 disabled:bg-black"
|
|
:disabled="isRequesting" @click="request">{{isRequesting ? "Processing..." : "Request"}}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</transition>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import type { IMovieDetails } from "@autoplex-api/request";
|
|
import { Status } from "@autoplex/restful";
|
|
import { defineComponent } from "vue";
|
|
import { getAverageRgb } from "../../util";
|
|
import { useStore, Mutation, Action } from "../../store";
|
|
import MoviePoster from "../MoviePoster.vue";
|
|
import { mapGetters } from "vuex";
|
|
|
|
export default defineComponent({
|
|
components: {
|
|
MoviePoster
|
|
},
|
|
computed: {
|
|
...mapGetters(["userId"]),
|
|
backdropOverlayStyle(): string {
|
|
let { r, g, b } = this.rgb;
|
|
return `background-image: linear-gradient(to right, rgba(${r}, ${g}, ${b}, 1.0), rgba(${r}, ${g}, ${b}, 0.84)`;
|
|
},
|
|
isDark(): boolean {
|
|
let { r, g, b } = this.rgb;
|
|
return (r*0.299 + g*0.587 + b*0.114) <= 186;
|
|
// r /= 255;
|
|
// g /= 255;
|
|
// b /= 255;
|
|
// r = r <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
|
|
// g = g <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
|
|
// b = b <= 0.03928 ? r/12.92 : Math.pow((r+0.055)/1.055, 2.4);
|
|
// let L = 0.2126*r + 0.7152*g + 0.0722*b;
|
|
// return L <= 0.179;
|
|
},
|
|
releaseYear(): string {
|
|
if (!this.movie || !this.movie.releaseDate) {
|
|
return "";
|
|
}
|
|
return this.movie.releaseDate.slice(0, 4);
|
|
},
|
|
releaseDate(): string {
|
|
if (!this.movie || !this.movie.releaseDate) {
|
|
return "";
|
|
}
|
|
let [year, month, day] = this.movie.releaseDate.split('-');
|
|
return `${month}/${day}/${year}`;
|
|
},
|
|
runtime(): string {
|
|
if (!this.movie || !this.movie.runtime) {
|
|
return "";
|
|
}
|
|
let hours = Math.floor(this.movie.runtime / 60);
|
|
let minutes = Math.floor(this.movie.runtime % 60);
|
|
return (hours > 0 ? `${hours}h ` : "") + `${minutes}m`;
|
|
}
|
|
},
|
|
data() {
|
|
return {
|
|
rgb: {r: 0, g: 0, b: 0},
|
|
movie: <IMovieDetails|undefined>undefined,
|
|
isPosterLoaded: false,
|
|
isRequesting: false
|
|
}
|
|
},
|
|
methods: {
|
|
lockScroll(lock: boolean) {
|
|
useStore().commit(Mutation.LockScroll, lock);
|
|
},
|
|
close() {
|
|
this.movie = undefined;
|
|
this.$emit("onClose");
|
|
},
|
|
onKeyPress(event: KeyboardEvent) {
|
|
if (event.key != "Escape") {
|
|
return;
|
|
}
|
|
this.close();
|
|
},
|
|
onPosterLoad(img: HTMLImageElement) {
|
|
this.isPosterLoaded = true;
|
|
this.computeBackdropColor(img);
|
|
},
|
|
computeBackdropColor(img: HTMLImageElement) {
|
|
// This is being weird and this is the only way I can fix it...
|
|
let rgb: {[k: string]: number} = {r: 0, g: 0, b: 0};
|
|
try {
|
|
rgb = getAverageRgb(img);
|
|
} catch(e) {
|
|
return { r: 0, g: 0, b: 0 };
|
|
}
|
|
|
|
// Adjust contrast
|
|
let c = 60;
|
|
let f = 259*(c + 255) / (255*(259 - c));
|
|
for (let k in rgb) {
|
|
rgb[k] = Math.min(Math.max(f*(rgb[k] - 128) + 128, 0), 255);
|
|
}
|
|
|
|
// Adjust brightness
|
|
for (let k in rgb) {
|
|
rgb[k] *= 0.90;
|
|
}
|
|
|
|
this.rgb = <any>rgb;
|
|
},
|
|
async fetchMovieDetails() {
|
|
if (this.tmdbId === undefined) {
|
|
return;
|
|
}
|
|
let [status, response] = await this.$store.dispatch(Action.MovieDetails, this.tmdbId);
|
|
if (status != Status.Ok) {
|
|
this.close();
|
|
return;
|
|
}
|
|
this.movie = response.result;
|
|
},
|
|
async cancel() {
|
|
if (this.isRequesting || this.movie == null || this.movie.ticketId == null || this.movie.requestedBy?.id != this.userId) {
|
|
return;
|
|
}
|
|
this.isRequesting = true;
|
|
let [status, response] = await this.$store.dispatch(Action.CancelMovieRequest, this.movie.ticketId);
|
|
this.isRequesting = false;
|
|
if (status != Status.Ok) {
|
|
console.error("Failed to cancel movie request");
|
|
return;
|
|
}
|
|
this.movie.ticketId = null;
|
|
this.movie.requestedBy = null;
|
|
this.$emit("onCancel", this.movie);
|
|
},
|
|
async request() {
|
|
if (this.isRequesting || this.movie == null || this.movie.tmdbId == null) {
|
|
return;
|
|
}
|
|
this.isRequesting = true;
|
|
let [status, response] = await this.$store.dispatch(Action.CreateMovieRequest, this.movie.tmdbId);
|
|
this.isRequesting = false;
|
|
if (status == Status.Forbidden) {
|
|
console.error("Failed to add movie: quota has been met");
|
|
return;
|
|
}
|
|
this.movie.ticketId = response.result.ticketId;
|
|
this.movie.requestedBy = {
|
|
id : this.$store.state.user!.id,
|
|
isAdmin: this.$store.state.user!.isAdmin,
|
|
name : this.$store.state.user!.name,
|
|
}
|
|
}
|
|
},
|
|
mounted() {
|
|
document.addEventListener("keydown", this.onKeyPress);
|
|
this.fetchMovieDetails();
|
|
},
|
|
beforeUnmount() {
|
|
document.removeEventListener("keydown", this.onKeyPress);
|
|
},
|
|
props: {
|
|
tmdbId: {
|
|
type: [Number, String],
|
|
required: false
|
|
}
|
|
},
|
|
watch: {
|
|
tmdbId() {
|
|
this.fetchMovieDetails();
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style lang="postcss">
|
|
.dot-separated > *:not(:first-child)::before {
|
|
@apply px-2;
|
|
content: '\2022'
|
|
}
|
|
|
|
.modal button {
|
|
@apply ring-0;
|
|
}
|
|
|
|
.modal button:focus-visible {
|
|
@apply ring-2 ring-indigo-500 ring-offset-2;
|
|
}
|
|
|
|
</style>
|