<template> <div class="page"> <div class="content"> <Banner :release="scene" /> <div class="meta"> <div class="entity"> <Link v-if="scene.channel" :href="`/${scene.channel.type}/${scene.channel.slug}`" class="channel-link entity-link nolink" > <img v-if="scene.channel.hasLogo" :src="scene.channel.isIndependent || !scene.network ? `/logos/${scene.channel.slug}/thumbs/network.png` : `/logos/${scene.network.slug}/thumbs/${scene.channel.slug}.png`" class="channel-logo entity-logo" > <span v-else class="name" >{{ scene.channel.name }}</span> </Link> <span v-if="!scene.channel.isIndependent && scene.network" class="network-container" > by <Link :href="`/${scene.network.type}/${scene.network.slug}`" class="network-link entity-link nolink" > <img v-if="scene.network.hasLogo" :src="`/logos/${scene.network.slug}/thumbs/network.png`" class="network-logo entity-logo" > <span v-else class="name" >{{ scene.network.name }}</span> </Link> </span> </div> <Link :href="scene.watchUrl" :title="scene.date ? formatDate(scene.date.toISOString(), 'y-MM-dd hh:mm') : `Release date unknown, added ${formatDate(scene.createdAt, 'y-MM-dd')}`" target="_blank" class="date nolink" :class="{ nodate: !scene.date }" :data-umami-event="scene.affiliate ? 'scene-date-click-aff' : 'scene-date-click'" :data-umami-event-aff-id="scene.affiliate?.id" :data-umami-event-scene-id="scene.id" > <time :datetime="scene.effectiveDate.toISOString()" class="ellipsis compact-hide" >{{ formatDate(scene.effectiveDate, { month: 'MMMM y', year: 'y', }[scene.datePrecision] || 'MMMM d, y') }}</time> <time :datetime="scene.effectiveDate.toISOString()" class="ellipsis compact-show" >{{ formatDate(scene.effectiveDate, { month: 'MMM y', year: 'y', }[scene.datePrecision] || 'MMM d, y') }}</time> </Link> </div> <div class="header"> <h2 v-if="scene.title" :title="scene.title" class="title" >{{ scene.title }}</h2> <h2 v-else-if="scene.actors.length > 0" class="title notitle" >Scene featuring {{ scene.actors.map((actor) => actor.name).join(', ') }}</h2> <h2 v-else class="title notitle" >No title</h2> <div class="actions"> <Heart domain="scenes" :item="scene" /> <div class="view"> <!-- <button v-if="scene.photos.length > 0" class="button view nolink" >View photos</button> --> <a v-if="scene.watchUrl" :href="scene.watchUrl" target="_blank" rel="noopener" class="button button-primary watch nolink" :data-umami-event="scene.affiliate ? 'watch-click-aff' : 'watch-click'" :data-umami-event-scene-id="`${(scene.channel || scene.network).slug}:scene:${scene.id}`" :data-umami-event-aff-id="scene.affiliate && `aff:${scene.affiliate.id}`" :data-umami-event-channel="scene.channel?.slug" :data-umami-event-network="scene.network?.slug" >Watch full video</a> </div> </div> </div> <div class="info"> <ul v-if="scene.actors.length > 0" class="actors nolist nobar" > <li v-for="actor in scene.actors" :key="`actor-${actor.id}`" class="actor" > <ActorTile :actor="actor" /> </li> </ul> <ul v-if="scene.tags.length > 0" class="tags nolist" > <li v-for="tag in scene.tags" :key="`tag-${tag.id}`" > <Link :href="`/tag/${tag.slug}`" class="tag nolink" >{{ tag.name }}</Link> </li> </ul> <div v-if="scene.movies.length > 0 || scene.series.length > 0" class="section" > <h3 class="heading">Part of</h3> <div class="movies"> <MovieTile v-for="movie in scene.movies" :key="`movie-${movie.id}`" :movie="movie" :show-details="false" /> </div> <div class="series"> <SerieTile v-for="serie in scene.series" :key="`serie-${serie.id}`" :serie="serie" :details="false" /> </div> </div> <div v-if="scene.duration || scene.directors.length > 0 || scene.shootId || scene.qualities.length > 0" class="section details" > <div v-if="scene.directors.length > 0" class="detail" > <h3 class="heading">Director</h3> {{ scene.directors.map((director) => director.name).join(', ') }} </div> <div v-if="scene.duration" class="detail" > <h3 class="heading">Duration</h3> {{ formatDuration(scene.duration) }} </div> <div v-if="scene.shootId" class="detail" > <h3 class="heading">Shoot</h3> {{ scene.shootId }} </div> <time v-if="scene.productionDate" :datetime="formatDate(scene.productionDate, 'yyyy-MM-dd')" :title="formatDate(scene.productionDate, 'yyyy-MM-dd')" class="detail" > <h3 class="heading">Shoot date</h3> {{ formatDate(scene.productionDate, 'MMMM d, yyyy') }} </time> <div v-if="scene.studio" class="detail" > <h3 class="heading">Studio</h3> <a :href="`/studio/${scene.studio.slug}`" class="link" >{{ scene.studio.name }}</a> </div> <div v-if="scene.qualities.length > 0" class="detail" > <h3 class="heading">Quality</h3> <template v-if="qualities[scene.qualities[0]]">{{ qualities[scene.qualities[0]] }} ({{ scene.qualities[0] }}p)</template> <template v-else>{{ scene.qualities[0] }}p</template> </div> <div v-if="scene.photoCount" class="detail" > <h3 class="heading">Photos</h3> {{ scene.photoCount }} </div> </div> <Chapters v-if="scene.chapters.length > 0" :chapters="scene.chapters" class="section" /> <div v-if="scene.description" class="section" > <h3 class="heading">Description</h3> <p class="description">{{ scene.description }}</p> </div> <div v-if="campaigns?.scene" class="section" > <Campaign :campaign="campaigns.scene" /> </div> <div class="section details"> <div class="detail"> <h3 class="heading">Added</h3> <span> <span class="added-date">{{ formatDate(scene.createdAt, 'yyyy-MM-dd') }}</span> <span :title="`Batch ${scene.createdBatchId}`" class="added-batch" >#{{ scene.createdBatchId }}</span> </span> </div> <div v-if="scene.comment" class="detail" > <h3 class="heading">Comment</h3> {{ scene.comment }} </div> </div> <div v-if="summary" class="section summary" > <h3 class="heading">Summary</h3> <div class="detail"> <input class="input" :value="summary" @focus="$event.target.select()" > <a v-if="user" class="icon-link" target="_blank" :href="`/user/${user.username}/templates?t=${selectedTemplate}`" > <Icon v-tooltip="'Edit templates'" icon="pencil5" class="edit" @click="showSummaryDialog = true" /> </a> <Icon v-tooltip="'Copy to clipboard'" icon="copy" class="copy" @click="copySummary" /> </div> <ul v-if="user && assets.templates.length > 0" class="nolist templates" > <Icon icon="markup" /> <li v-for="userTemplate in templates" :key="`template-${userTemplate.id}`" class="template" :class="{ selected: userTemplate.id === selectedTemplate }" @click="selectTemplate(userTemplate.id)" >{{ userTemplate.name }}</li> </ul> </div> <div class="scene-actions section" > <a v-if="user && user.role !== 'user'" :href="`/scene/edit/${scene.id}/${scene.slug}`" target="_blank" class="link" >Edit scene</a> <a :href="`/scene/revs/${scene.id}/${scene.slug}`" target="_blank" class="link" >Revisions</a> </div> </div> </div> </div> </template> <script setup> import { ref, inject } from 'vue'; import Cookies from 'js-cookie'; import { formatDate, formatDuration } from '#/utils/format.js'; import events from '#/src/events.js'; import processSummaryTemplate from '#/utils/process-summary-template.js'; import Banner from '#/components/media/banner.vue'; import ActorTile from '#/components/actors/tile.vue'; import MovieTile from '#/components/movies/tile.vue'; import SerieTile from '#/components/series/tile.vue'; import Chapters from '#/components/scenes/chapters.vue'; import Heart from '#/components/stashes/heart.vue'; import Campaign from '#/components/campaigns/campaign.vue'; import defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved const cookies = Cookies.withConverter({ write: (value) => value, }); const { pageProps, campaigns, user, assets, env, } = inject('pageContext'); const { scene } = pageProps; const showSummaryDialog = ref(false); const qualities = { 2160: '4K', 1440: 'Quad HD', 1080: 'Full HD', 720: 'HD', 576: 'PAL VHS', 540: 'qHD', 480: 'VGA', 360: 'nHD', }; const summary = ref(null); const selectedTemplate = ref(null); const templates = [ { id: 0, name: 'traxxx', template: defaultTemplate, }, ...(assets?.templates || []), ]; function selectTemplate(templateId, allowFallback = true) { try { const targetTemplate = templates.find((userTemplate) => userTemplate.id === templateId); if (!targetTemplate && !allowFallback) { return; } const template = targetTemplate || templates[0]; summary.value = processSummaryTemplate(template.template, scene); selectedTemplate.value = template.id; cookies.set('selectedTemplate', String(templateId)); } catch (error) { console.error(`Failed to process summary template: ${error.message}`); summary.value = null; } } selectTemplate(env.selectedTemplate); function copySummary() { navigator.clipboard.writeText(summary.value); events.emit('feedback', { type: 'success', message: 'Summary copied to clipboard', }); } </script> <style scoped> .page { display: flex; justify-content: center; flex-grow: 1; background: var(--background-base-10); } .content { width: 100%; max-width: 1200px; margin: 0 1rem; display: flex; flex-direction: column; } .meta { display: flex; height: 3.25rem; justify-content: space-between; align-items: stretch; background: var(--grey-dark-40); border-radius: 0 0 .5rem .5rem; color: var(--text-light); overflow: hidden; } .entity { display: flex; align-items: center; font-weight: bold; color: var(--highlight); } .entity-link { display: flex; align-items: center; box-sizing: border-box; padding: .5rem 1rem; height: 100%; color: var(--text-light); } .entity-logo { max-width: 15rem; max-height: 100%; } .network-container { height: 100%; display: flex; align-items: center; overflow: hidden; } .date { padding: 1rem; font-weight: bold; } .nodate { color: var(--highlight); font-weight: normal; } .info, .header { border-top: none; border-bottom: none; } .info { padding: 0; } .header { display: flex; align-items: center; justify-content: space-between; padding: 1rem .5rem 1rem .5rem; } .title { margin: .25rem .5rem .5rem 0; line-height: 1.25; display: -webkit-box; &:not(:active) { -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } } .notitle { color: var(--grey-dark-10); } .actions { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; .button { flex-shrink: 0; padding: .75rem; } .button:not(:last-child) { margin-right: .5rem; } } .bookmarks { margin-right: .75rem; } .view { display: flex; } .watch.button { padding: .75rem 2rem; margin-left: .25rem; font-size: 1rem; } .actors, .tags { margin-bottom: 1rem; } .actors { display: flex; flex-grow: 1; gap: .25rem; overflow-x: auto; .actor { width: 10rem; flex-shrink: 0; } } .tag { padding: .5rem; border-radius: .25rem; margin: 0 .25rem .25rem 0; background: var(--background); box-shadow: 0 0 3px var(--shadow-weak-30); &:hover { color: var(--primary); box-shadow: 0 0 3px var(--shadow-weak-20); cursor: pointer; } } .movies, .series { display: grid; flex-grow: 1; gap: .25rem; } .movies { grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); } .series { grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); } .section { margin-bottom: 1rem; } .heading { color: var(--primary); margin: 0 0 .5rem 0; font-size: .9rem; } .details { display: flex; gap: 1rem 1.5rem; flex-wrap: wrap; } .detail { display: flex; flex-direction: column; } .description { font-size: 1rem; line-height: 1.5; text-align: justify; margin: 0; } .added-batch { color: var(--glass-weak-10); margin-left: .25rem; } .summary { .detail { display: flex; align-items: stretch; flex-direction: row; } .input { flex-grow: 1; } .detail .icon { height: auto; padding: 0 .5rem 0 .75rem; fill: var(--glass); &:hover { cursor: pointer; fill: var(--primary); } } } .templates { display: flex; flex-wrap: wrap; align-items: center; gap: .25rem 0; margin-top: .5rem; .icon { width: 1.2rem; height: 1.2rem; margin-right: .5rem; fill: var(--glass-weak-10); } } .template { padding: .25rem .5rem; border-radius: .25rem; cursor: pointer; &:hover { color: var(--primary); } &.selected { background: var(--primary); color: var(--text-light); } } .scene-actions { display: flex; justify-content: center; gap: 2rem; margin-top: 1rem; } .icon-link { display: flex; height: auto; } .compact-show { display: none; } @media(--compact) { .content { margin: 0; } .info { margin: 0 .75rem; } .network-container { display: none; } .header { flex-direction: column-reverse; } .actions { width: 100%; justify-content: space-between; margin-bottom: 1.25rem; } .title { width: 100%; margin-left: 1rem; white-space: wrap; } .meta { border-radius: 0; } .entity-logo { width: 7.5rem; } } @media(--small-10) { .header { padding: 1rem .5rem .5rem .5rem; } .title { margin-left: 1.5rem; } .info { padding: 0 .5rem; } .actors { .actor { width: 9rem; } } .series { grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); } } @media(--small-50) { .date { font-size: .9rem; } } @media(--small-60) { .compact-show { display: flex; } .compact-hide { display: none; } } </style>