Showing chapter information.
This commit is contained in:
parent
63b1198fca
commit
3aedfbf2d1
|
@ -49,12 +49,12 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="release.photos.length > 0 || release.caps.length > 0 || coversInAlbum"
|
||||
v-if="release.photos.length > 0 || release.caps.length > 0 || release.chapters.some((chapter) => chapter.poster) || coversInAlbum"
|
||||
class="album"
|
||||
:class="{ single: (release.photos.length + release.caps.length) === 1 }"
|
||||
:class="{ single: (release.photos.length + release.caps.length + release.chapters.filter((chapter) => chapter.poster).length) === 1 }"
|
||||
>
|
||||
<div
|
||||
v-for="photo in [...(coversInAlbum ? release.covers : []), ...release.photos, ...release.caps]"
|
||||
v-for="photo in [...(coversInAlbum ? release.covers : []), ...release.photos, ...release.caps, ...release.chapters.map((chapter) => chapter.poster).filter(Boolean)]"
|
||||
:key="`photo-${photo.id}`"
|
||||
class="photo-container"
|
||||
>
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="timeline"
|
||||
class="timeline"
|
||||
>
|
||||
<ul class="timeline-items nolist">
|
||||
<li
|
||||
v-for="chapter in timeline"
|
||||
:key="`chapter-${chapter.id}`"
|
||||
:style="{ left: `${(chapter.time / duration) * 100}%` }"
|
||||
:title="formatDuration(chapter.time)"
|
||||
class="timeline-item"
|
||||
><a
|
||||
:href="`/tag/${chapter.tags[0].slug}`"
|
||||
class="link"
|
||||
>{{ chapter.tags[0]?.name || ' ' }}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-else
|
||||
class="chapters nolist"
|
||||
>
|
||||
<li
|
||||
v-for="chapter in chapters"
|
||||
:key="`chapter-${chapter.id}`"
|
||||
class="chapter"
|
||||
>
|
||||
<img
|
||||
:src="getPath(chapter.poster, 'thumbnail')"
|
||||
:style="{ 'background-image': `url('${getPath(chapter.poster, 'lazy')}'` }"
|
||||
loading="lazy"
|
||||
class="chapter-poster"
|
||||
>
|
||||
|
||||
<span class="chapter-details">
|
||||
<span
|
||||
v-if="typeof chapter.time === 'number'"
|
||||
v-tooltip="'Time in video'"
|
||||
class="chapter-time"
|
||||
><Icon icon="film3" /> {{ formatDuration(chapter.time) }}</span>
|
||||
|
||||
<span
|
||||
v-if="chapter.duration"
|
||||
v-tooltip="'Duration'"
|
||||
class="chapter-duration"
|
||||
><Icon icon="stopwatch" />{{ formatDuration(chapter.duration) }}</span>
|
||||
</span>
|
||||
|
||||
<div class="chapter-info">
|
||||
<h3
|
||||
v-if="chapter.title"
|
||||
class="chapter-row chapter-title"
|
||||
:title="chapter.title"
|
||||
>{{ chapter.title }}</h3>
|
||||
|
||||
<p
|
||||
v-if="chapter.description"
|
||||
class="chapter-row chapter-description"
|
||||
>{{ chapter.description }}</p>
|
||||
|
||||
<ul class="chapter-tags chapter-row nolist">
|
||||
<li
|
||||
v-for="tag in chapter.tags"
|
||||
:key="`chapter-tag-${tag.slug}`"
|
||||
class="chapter-tag"
|
||||
>
|
||||
<a
|
||||
:href="`/tag/${tag.slug}`"
|
||||
class="link"
|
||||
>{{ tag.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDuration } from '#/utils/format.js';
|
||||
|
||||
const props = defineProps({
|
||||
chapters: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const lastChapter = props.chapters.at(-1);
|
||||
const duration = lastChapter.time + lastChapter.duration;
|
||||
|
||||
const timeline = computed(() => {
|
||||
if (props.chapters.every((chapter) => chapter.time)) {
|
||||
return props.chapters.filter((chapter) => chapter.tags?.length > 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chapters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.chapter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
border-radius: .25rem;
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chapter-poster {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.chapter-details {
|
||||
height: 1.75rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 .5rem;
|
||||
border-radius: 0 0 .25rem .25rem;
|
||||
margin: 0 0 .5rem 0;
|
||||
color: var(--text-light);
|
||||
background: var(--grey-dark-40);
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
|
||||
.icon {
|
||||
fill: var(--text-light);
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-duration,
|
||||
.chapter-time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chapter-duration .icon {
|
||||
/* narrower icon */
|
||||
margin: -.1rem .3rem 0 0;
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
padding: 0 .5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.chapter-row {
|
||||
margin: 0 0 .5rem 0;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
padding: 0;
|
||||
font-size: .9rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chapter-description {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.chapter-tags {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chapter-tag {
|
||||
margin: 0 .5rem .25rem 0;
|
||||
font-size: .75rem;
|
||||
color: var(--glass-strong-10);
|
||||
|
||||
.link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-items {
|
||||
position: relative;
|
||||
height: 5rem;
|
||||
border-bottom: solid 1px var(--shadow-weak);
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: absolute;
|
||||
bottom: -.25rem;
|
||||
padding: .1rem .5rem;
|
||||
border-radius: .6rem;
|
||||
color: var(--primary);
|
||||
background: var(--background);
|
||||
transform: rotate(-60deg);
|
||||
transform-origin: 0 50%;
|
||||
box-shadow: 0 0 3px var(--shadow-weak);
|
||||
font-size: .8rem;
|
||||
font-weight: bold;
|
||||
|
||||
.link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 1rem;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: calc(-1rem + 1px);
|
||||
margin: .3rem .5rem 0 0;
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
@media(--small-30) {
|
||||
.chapters {
|
||||
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -168,6 +168,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Chapters
|
||||
v-if="scene.chapters.length > 0"
|
||||
:chapters="scene.chapters"
|
||||
class="section"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="scene.duration || scene.directors.length > 0 || scene.shootId || scene.qualities.length > 0"
|
||||
class="section details"
|
||||
|
@ -361,6 +367,7 @@ 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';
|
||||
|
||||
|
@ -380,6 +387,8 @@ const {
|
|||
|
||||
const { scene } = pageProps;
|
||||
|
||||
console.log(scene);
|
||||
|
||||
const showSummaryDialog = ref(false);
|
||||
|
||||
const qualities = {
|
||||
|
|
|
@ -58,6 +58,8 @@ function curateScene(rawScene, assets) {
|
|||
return null;
|
||||
}
|
||||
|
||||
console.log(assets.chapters);
|
||||
|
||||
const curatedScene = {
|
||||
id: rawScene.id,
|
||||
title: rawScene.title,
|
||||
|
@ -113,6 +115,18 @@ function curateScene(rawScene, assets) {
|
|||
name: tag.name,
|
||||
priority: tag.priority,
|
||||
})),
|
||||
chapters: assets.chapters.map((chapter) => ({
|
||||
id: chapter.id,
|
||||
title: chapter.title,
|
||||
time: chapter.time,
|
||||
duration: chapter.duration,
|
||||
poster: curateMedia(chapter.chapter_poster),
|
||||
tags: chapter.chapter_tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
slug: tag.slug,
|
||||
})),
|
||||
})),
|
||||
qualities: rawScene.qualities?.sort((qualityA, qualityB) => qualityB - qualityA) || [],
|
||||
photoCount: rawScene.photo_count,
|
||||
movies: assets.movies.map((movie) => ({
|
||||
|
@ -151,6 +165,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||
actors,
|
||||
directors,
|
||||
tags,
|
||||
chapters,
|
||||
movies,
|
||||
series,
|
||||
posters,
|
||||
|
@ -202,6 +217,19 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||
.whereNotNull('tags.id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.orderBy('priority', 'desc'),
|
||||
chapters: knex('chapters')
|
||||
.select(
|
||||
'chapters.*',
|
||||
knex.raw('coalesce(json_agg(tags) filter (where tags.id is not null), \'[]\') as chapter_tags'),
|
||||
knex.raw('row_to_json(posters) as chapter_poster'),
|
||||
)
|
||||
.leftJoin('chapters_tags', 'chapters_tags.chapter_id', 'chapters.id')
|
||||
.leftJoin('tags', 'tags.id', 'chapters_tags.tag_id')
|
||||
.leftJoin('chapters_posters', 'chapters_posters.chapter_id', 'chapters.id')
|
||||
.leftJoin('media as posters', 'posters.id', 'chapters_posters.media_id')
|
||||
.whereIn('chapters.release_id', sceneIds)
|
||||
.groupBy('chapters.id', 'posters.id')
|
||||
.orderBy('time', 'asc'),
|
||||
movies: context.includePartOf ? knex('movies_scenes')
|
||||
.select('movies_scenes.scene_id', 'movies.*', knex.raw('json_agg(media) as movie_covers'))
|
||||
.leftJoin('movies', 'movies.id', 'movies_scenes.movie_id')
|
||||
|
@ -289,6 +317,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||
const sceneActors = actors.filter((actor) => actor.release_id === sceneId);
|
||||
const sceneDirectors = directors.filter((director) => director.release_id === sceneId);
|
||||
const sceneTags = tags.filter((tag) => tag.release_id === sceneId);
|
||||
const sceneChapters = chapters.filter((chapter) => chapter.release_id === sceneId);
|
||||
const sceneMovies = movies.filter((movie) => movie.scene_id === sceneId);
|
||||
const sceneSeries = series.filter((serie) => serie.scene_id === sceneId);
|
||||
const scenePoster = posters.find((poster) => poster.release_id === sceneId);
|
||||
|
@ -305,6 +334,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||
actors: sceneActors,
|
||||
directors: sceneDirectors,
|
||||
tags: sceneTags,
|
||||
chapters: sceneChapters,
|
||||
movies: sceneMovies,
|
||||
series: sceneSeries,
|
||||
poster: scenePoster,
|
||||
|
|
Loading…
Reference in New Issue