<template> <div v-if="release" ref="content" class="content" @scroll="events.emit('scroll', $event)" > <Scroll v-slot="slotProps" class="scroll-light banner" :style="{ 'background-image': bannerBackground }" :expandable="false" > <Banner :release="release" class="media" @load="slotProps.loaded" /> </Scroll> <Details :release="release" /> <button v-if="release.photos?.length > 0 || release.scenesPhotos?.length > 0" class="album-toggle" @click="$router.push({ hash: '#album' })" ><Icon icon="grid3" />View album</button> <Album v-if="showAlbum" :items="[release.poster, ...(release.photos || []), ...(release.scenesPhotos || [])]" :title="release.title" :path="config.media.mediaPath" @close="$router.replace({ hash: undefined })" /> <div class="info column"> <div class="row row-title"> <h2 v-if="release.title" class="title" > {{ release.title }} <template v-if="release.movies?.[0]?.title && /^scene \d+$/i.test(release.title)"><span class="title-composed"> from </span>{{ release.movies[0].title }}</template> </h2> <h2 v-else-if="release.actors.length > 0" class="title title-composed" > {{ release.actors.map(actor => actor.name).join(', ') }} for {{ release.entity.name }} <Icon v-tooltip="`This scene has no known official title`" icon="question2" /> </h2> <StashButton :stashed-by="stashedBy" @stash="(stash) => stashScene(stash)" @unstash="(stash) => unstashScene(stash)" /> </div> <Releases v-if="release.scenes && release.scenes.length > 0" :releases="release.scenes" /> <div class="row associations"> <ul ref="actors" class="actors nolist noselect bar-inline" @mousedown.prevent="startActorsScroll" > <li v-for="actor in release.actors" :key="actor.id" > <Actor :actor="actor" :has-scrolled="actorsHasScrolled" /> </li> </ul> </div> <Tags v-if="release.tags.length > 0" :tags="release.tags" /> <div v-if="release.movies?.length > 0 || release.series?.length > 0" class="row" > <span class="row-label">Part of</span> <div class="movies"> <RouterLink v-for="movie in [...release.movies, ...release.series]" :key="`movie-${movie.id}`" :to="{ name: movie.type || 'movie', params: { releaseId: movie.id, releaseSlug: movie.slug } }" class="movie" > <span class="movie-title">{{ movie.title }}</span> <img v-if="movie.covers.length > 0 || movie.poster" :src="getPath(movie.covers[0] || movie.poster, 'thumbnail')" class="movie-cover" > </RouterLink> </div> </div> <div v-if="release.directors && release.directors.length > 0" class="row" > <span class="row-label">Director</span> <RouterLink v-for="director in release.directors" :key="`director-${director.id}`" class="link director" :to="`/director/${director.id}/${director.slug}`" >{{ director.name }}</RouterLink> </div> <div v-if="release.description" class="row" > <span class="row-label">Description</span> <p class="description">{{ release.description }}</p> </div> <div v-if="release.chapters?.length > 0" class="row nolist" > <span class="row-label">Chapters</span> <Chapters :chapters="release.chapters" /> </div> <div class="row row-tidbits"> <div v-if="release.duration" class="row-tidbit" > <span class="row-label">Duration</span> <div class="duration">{{ formatDuration(release.duration) }}</div> </div> <div v-if="release.shootId" class="row-tidbit" > <span class="row-label">Shoot #</span> {{ release.shootId }} </div> <div v-if="release.studio" class="row-tidbit" > <span class="row-label">Studio</span> <RouterLink :to="`/studio/${release.studio.slug}`" class="link studio" >{{ release.studio.name }}</RouterLink> </div> <div v-if="release.productionDate" class="row-tidbit" > <span class="row-label">Shoot date</span> {{ formatDate(release.productionDate, 'MMMM D, YYYY') }} </div> <div v-if="release.productionLocation" class="row-tidbit" > <span class="row-label">Location</span> <span class="location"> <span v-if="release.productionLocation.city" class="location-segment" >{{ release.productionLocation.city }}, </span> <span v-if="release.productionLocation.state" class="location-segment" >{{ release.productionLocation.state }}, </span> <span v-if="release.productionLocation.country" class="location-segment" >{{ release.productionLocation.country.alias || release.productionLocation.country.name }} <img class="flag" :src="`/img/flags/${release.productionLocation.country.alpha2.toLowerCase()}.svg`" > </span> </span> </div> </div> <div v-if="release.qualities?.length > 0" class="row" > <span class="row-label">Available qualities</span> <span v-for="quality in release.qualities" :key="quality" class="quality" >{{ quality }}</span> </div> <div v-if="release.comment" class="row" > <span class="row-label">Comment</span> <span>{{ release.comment }}</span> </div> <div class="row"> <span class="row-label">Added</span> <RouterLink :to="`/added/${formatDate(release.createdAt, 'YYYY/MM/DD')}`" :title="`Added on ${formatDate(release.createdAt, 'MMMM D, YYYY HH:mm')}`" class="link added" >{{ release.createdBatchId }}: {{ formatDate(release.createdAt, 'MMMM D, YYYY HH:mm') }}</RouterLink> </div> <div class="row"> <span class="row-label">Summary</span> <div class="summary"> <input ref="summary" v-model="summary" class="input" @focus="selectSummary" > <button v-if="hasClipboard" type="button" class="button button-secondary" :disabled="summaryCopied" @focus="copySummary" >{{ summaryCopied ? 'Copied!' : 'Copy' }}</button> </div> </div> </div> </div> </template> <script> import formatSummary from '../../js/utils/format-summary'; import Details from './details.vue'; import Banner from './banner.vue'; import StashButton from '../stashes/button.vue'; import Album from '../album/album.vue'; import Tags from './tags.vue'; import Chapters from './chapters.vue'; import Actor from '../actors/tile.vue'; import Releases from './releases.vue'; import Scroll from '../scroll/scroll.vue'; async function fetchRelease(scroll = true) { if (this.$route.name === 'scene') { this.release = await this.$store.dispatch('fetchReleaseById', this.$route.params.releaseId); } if (this.$route.name === 'movie') { this.release = await this.$store.dispatch('fetchMovieById', this.$route.params.releaseId); } if (this.$route.name === 'serie') { this.release = await this.$store.dispatch('fetchSerieById', this.$route.params.releaseId); } if (scroll && this.$refs.content) { this.$refs.content.scrollTop = 0; } this.stashedBy = this.release.stashes; this.setSummary(); } async function stashScene(stashId) { this.stashedBy = await this.$store.dispatch(this.$route.name === 'movie' ? 'stashMovie' : 'stashScene', { sceneId: this.release.id, movieId: this.release.id, stashId, }); } async function unstashScene(stashId) { this.stashedBy = await this.$store.dispatch(this.$route.name === 'movie' ? 'unstashMovie' : 'unstashScene', { sceneId: this.release.id, movieId: this.release.id, stashId, }); } function startActorsScroll(event) { event.preventDefault(); this.$refs.actors.addEventListener('mousemove', this.scrollActors); document.addEventListener('mouseup', this.endActorsScroll); this.actorsScrollStart = { mouse: event.clientX, scroll: this.$refs.actors.scrollLeft }; this.actorsHasScrolled = false; } function scrollActors(event) { this.$refs.actors.scrollLeft = this.actorsScrollStart.scroll + this.actorsScrollStart.mouse - event.clientX; if (Math.abs(this.actorsScrollStart.mouse - event.clientX) > 10) { this.actorsHasScrolled = true; } } function endActorsScroll() { this.$refs.actors.removeEventListener('mousemove', this.scrollActors); document.removeEventListener('mouseup', this.endActorsScroll); } function me() { return this.$store.state.auth.user; } function setSummary() { // this.summary = `${this.release.entity.name} - ${this.release.title} (${this.release.actors.map((actor) => actor.name).join(', ')}, ${this.formatDate(this.release.date, 'DD-MM-YYYY')})`; const simpleRelease = { channel: this.release.entity.name, network: this.release.entity.parent?.name || this.release.entity.name, title: this.release.title, movie: this.release.movies?.[0]?.title, actors: this.release.actors.map((actor) => actor.name), tags: this.release.tags.map((tag) => tag.name), date: this.release.date, }; this.summary = formatSummary(simpleRelease, this.$store.state.ui.summaryFormat); } async function selectSummary() { this.$refs.summary.select(); } async function copySummary() { const { state } = await navigator.permissions.query({ name: 'clipboard-write' }); if (state === 'granted' || state === 'prompt') { await navigator.clipboard.writeText(this.summary); this.summaryCopied = true; setTimeout(() => { this.summaryCopied = false; }, 1000); } } function bannerBackground() { return (this.release.poster && this.getBgPath(this.release.poster, 'thumbnail')) || (this.release.covers.length > 0 && this.getBgPath(this.release.covers[0], 'thumbnail')); } function pageTitle() { return this.release && (this.release.title || (this.release.actors.length > 0 ? `${this.release.actors.map((actor) => actor.name).join(', ')} for ${this.release.entity.name}` : null)); } function showAlbum() { return (this.release.photos?.length > 0 || this.release.scenesPhotos?.length > 0) && this.$route.hash === '#album'; } async function mounted() { this.fetchRelease(); } export default { components: { Actor, Album, Banner, Chapters, Details, Releases, Scroll, StashButton, Tags, }, data() { return { release: null, summary: null, summaryCopied: false, stashedBy: [], actorsScrollStart: 0, actorsHasScrolled: false, hasClipboard: !!navigator?.clipboard?.writeText, }; }, computed: { pageTitle, bannerBackground, me, showAlbum, }, watch: { $route: fetchRelease, }, mounted, methods: { copySummary, endActorsScroll, fetchRelease, scrollActors, selectSummary, setSummary, startActorsScroll, stashScene, unstashScene, }, }; </script> <style lang="scss" scoped> @import 'breakpoints'; .expand-bottom { border-bottom: solid 1px var(--shadow-hint); } .banner { background-position: center; background-size: cover; :deep(.scrollable) { backdrop-filter: blur(1rem); } } .info { padding: 1rem 0; border-left: solid 1px var(--shadow-hint); border-right: solid 1px var(--shadow-hint); flex-grow: 1; } .row { padding: 0 1rem; margin: 0 0 1rem 0; &.associations { align-items: start; } } .row-label { display: block; margin: 0 0 .5rem 0; color: var(--shadow); font-weight: bold; .icon { margin: 0 .5rem 0 0; fill: var(--shadow); } } .row-tidbit { display: inline-block; margin: 0 2rem 0 0; } .row-title { display: flex; justify-content: space-between; } .title { display: inline-flex; margin: 0; font-size: 1.5rem; line-height: 1.25; .icon { fill: var(--shadow); padding: .25rem; &:hover { fill: var(--primary); cursor: pointer; } } } .title-composed { color: var(--shadow); } .album-toggle { height: fit-content; display: inline-flex; align-items: center; justify-content: center; padding: .5rem 1rem; border: none; border-bottom: solid 1px var(--shadow-hint); color: var(--shadow); background: none; font-size: 1rem; font-weight: bold; .icon { fill: var(--shadow); margin: -.1rem .5rem 0 0; } &:hover { background: var(--shadow-hint); cursor: pointer; } } .description { line-height: 1.5; margin: -.25rem 0 0 0; } .actors { display: block; padding-bottom: .25rem; overflow-x: auto; white-space: nowrap; .actor { width: 10rem; margin-right: .25rem; user-select: none; } } .movies { display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); grid-gap: .5rem; flex-grow: 1; flex-wrap: wrap; } .movie { display: flex; flex-direction: column; background: var(--background); box-shadow: 0 0 3px var(--shadow-weak); color: var(--text); text-decoration: none; &:hover .movie-title { color: var(--primary); } } .movie-cover { width: 100%; } .movie-title { padding: .5rem; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .quality { &::after { content: 'p, '; } &:last-child::after { content: 'p', } } .releases { margin: 0 0 .5rem 0; } .flag { height: 1rem; margin: 0 0 -.15rem .1rem; } .summary { display: flex; .input { flex-grow: 1; } .button { width: 4rem; } } .link { display: inline-flex; color: var(--link); text-decoration: none; &.director:not(:last-child)::after { content: ', '; } &:hover { color: var(--primary); .icon { fill: var(--primary); } } } .showable { display: none; } @media(max-width: $breakpoint-kilo) { .releases { padding: .5rem; } } @media(max-width: $breakpoint) { .hideable { display: none; } .row .showable { display: block; } .tidbit .showable { display: inline-block; } .title { font-size: 1.25rem; } .actors { grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); } } </style>