Improved actor bio responsiveness, showing secondary actor photos.

This commit is contained in:
DebaucheryLibrarian 2024-06-07 03:15:37 +02:00
parent af88455c6b
commit 8f4533a1f8
5 changed files with 440 additions and 448 deletions

View File

@ -3,6 +3,7 @@
@custom-media --small-40 (max-width: 480px); @custom-media --small-40 (max-width: 480px);
@custom-media --small-30 (max-width: 540px); @custom-media --small-30 (max-width: 540px);
@custom-media --small-20 (max-width: 650px); @custom-media --small-20 (max-width: 650px);
@custom-media --small-15 (max-width: 720px);
@custom-media --small-10 (max-width: 768px); @custom-media --small-10 (max-width: 768px);
@custom-media --small (max-width: 900px); @custom-media --small (max-width: 900px);
@custom-media --compact (max-width: 1200px); @custom-media --compact (max-width: 1200px);

View File

@ -1,25 +1,18 @@
<template> <template>
<div
class="content-inner actor-inner"
@scroll="events.emit('scroll', $event)"
>
<div <div
class="profile" class="profile"
:class="{ expanded, 'with-avatar': !!actor.avatar }" :class="{ expanded, 'with-avatar': !!actor.avatar }"
> >
<a <div
v-if="actor.avatar" v-if="actor.avatar"
:href="getMediaPath(actor.avatar)" class="avatar-container"
target="_blank"
rel="noopener noreferrer"
class="avatar-link"
> >
<img <img
:src="getMediaPath(actor.avatar, 'thumbnail')" :src="getMediaPath(actor.avatar, 'thumbnail')"
:title="actor.avatar.credit && `© ${actor.avatar.credit}`" :title="actor.avatar.credit && `© ${actor.avatar.credit}`"
class="avatar" class="avatar"
> >
</a> </div>
<ul class="bio nolist"> <ul class="bio nolist">
<li <li
@ -28,10 +21,15 @@
> >
<dfn class="bio-label"><Icon icon="cake" />Date of birth</dfn> <dfn class="bio-label"><Icon icon="cake" />Date of birth</dfn>
<span class="birthdate">{{ formatDate(actor.dateOfBirth, 'MMMM d, yyyy') }}<span <span class="birthdate">
<span class="birthdate-long">{{ formatDate(actor.dateOfBirth, 'MMMM d, yyyy') }}</span>
<span class="birthdate-short">{{ formatDate(actor.dateOfBirth, 'MMM d, yyyy') }}</span>
<span
v-if="!actor.dateOfDeath" v-if="!actor.dateOfDeath"
class="age" class="age"
>{{ actor.ageFromBirth }}</span></span> >{{ actor.ageFromBirth }}</span>
</span>
</li> </li>
<li <li
@ -89,7 +87,10 @@
<img <img
class="flag" class="flag"
:src="`/img/flags/${actor.origin.country.alpha2.toLowerCase()}.svg`" :src="`/img/flags/${actor.origin.country.alpha2.toLowerCase()}.svg`"
>{{ actor.origin.country.alias || actor.origin.country.name }} >
<span class="country-name">{{ actor.origin.country.alias || actor.origin.country.name }}</span>
<span class="country-alpha2">{{ actor.origin.country.alpha2 }}</span>
</span> </span>
</span> </span>
</li> </li>
@ -238,7 +239,7 @@
<span v-else>Yes</span> <span v-else>Yes</span>
</li> </li>
<li class="bio-item scraped hideable">Updated {{ formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm') }}, ID: {{ actor.id }}</li> <li class="bio-item updated hideable">Updated {{ formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm') }}, ID: {{ actor.id }}</li>
</ul> </ul>
<div class="descriptions-container"> <div class="descriptions-container">
@ -268,6 +269,23 @@
</p> </p>
</div> </div>
</div> </div>
<div class="expand-container">
<button
type="button"
class="expand"
@click="expanded = !expanded"
>
<Icon
v-show="expanded"
icon="arrow-up3"
/>
<Icon
v-show="!expanded"
icon="arrow-down3"
/>
</button>
</div> </div>
</div> </div>
</template> </template>
@ -278,7 +296,7 @@ import { ref } from 'vue';
import { getMediaPath } from '#/utils/media-path.js'; import { getMediaPath } from '#/utils/media-path.js';
import { formatDate } from '#/utils/format.js'; import { formatDate } from '#/utils/format.js';
const expanded = ref(true); const expanded = ref(false);
defineProps({ defineProps({
actor: { actor: {
@ -289,10 +307,18 @@ defineProps({
</script> </script>
<style> <style>
.header-gender .icon { .header-gender {
display: inline-flex;
align-items: center;
margin: 0 0 0 .5rem;
transform: translate(0, .125rem);
font-size: 0;
.icon {
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
} }
}
</style> </style>
<style scoped> <style scoped>
@ -304,12 +330,13 @@ defineProps({
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-shrink: 0; flex-shrink: 0;
position: relative;
&.with-avatar { &.with-avatar {
height: 18rem; /* profile overlaps avatar in chrome */ height: 18rem; /* profile overlaps avatar in chrome */
} }
.avatar-link { .avatar-container {
padding: 0 0 1rem 1rem; padding: 0 0 1rem 1rem;
flex-shrink: 0; flex-shrink: 0;
} }
@ -320,6 +347,11 @@ defineProps({
border: solid 3px var(--highlight-hint); border: solid 3px var(--highlight-hint);
margin: 0 .5rem 0 0; margin: 0 .5rem 0 0;
} }
&.expanded {
padding-bottom: 1.5rem;
margin-bottom: .75rem;
}
} }
.bio { .bio {
@ -404,10 +436,14 @@ defineProps({
display: block; display: block;
} }
.birthdate-short {
display: none;
}
.age { .age {
font-weight: bold; font-weight: bold;
padding: 0 0 0 .5rem; padding: 0 0 0 .5rem;
border-left: solid 1px var(--highlight-weak); border-left: solid 1px var(--highlight-weak-20);
margin: 0 0 0 .5rem; margin: 0 0 0 .5rem;
} }
@ -416,6 +452,10 @@ defineProps({
justify-content: flex-end; justify-content: flex-end;
} }
.country-alpha2 {
display: none;
}
.figure .bio-label .icon { .figure .bio-label .icon {
margin: -.5rem .5rem 0 0; margin: -.5rem .5rem 0 0;
} }
@ -451,8 +491,8 @@ defineProps({
content: ',\00a0'; content: ',\00a0';
} }
.scraped { .updated {
color: var(--highlight-weak); color: var(--highlight-weak-20);
font-size: .8rem; font-size: .8rem;
} }
@ -519,8 +559,33 @@ defineProps({
display: none; display: none;
} }
.expand { .expand-container {
width: 100%;
display: none; display: none;
justify-content: center;
position: absolute;
bottom: -.75rem;
}
.expand {
width: 4rem;
height: 2rem;
display: inline-flex;
justify-content: center;
align-items: center;
border: none;
border-radius: .5rem;
background: var(--grey-dark-50);
box-shadow: 0 0 3px var(--shadow);
.icon {
fill: var(--text-light);
}
&:hover {
cursor: pointer;
background: var(--primary);
}
} }
.scroll { .scroll {
@ -544,15 +609,14 @@ defineProps({
} }
} }
/* @media(--big) {
@media(max-width: $breakpoint4) {
.descriptions-container { .descriptions-container {
display: none; display: none;
} }
} }
@media(max-width: $breakpoint3) { @media(--compact) {
.profile .avatar-link { .profile .avatar-container {
display: none; display: none;
} }
@ -561,7 +625,7 @@ defineProps({
} }
} }
@media(max-width: $breakpoint) { @media(--small-15) {
.profile { .profile {
height: auto; height: auto;
max-height: none; max-height: none;
@ -593,8 +657,8 @@ defineProps({
white-space: normal; white-space: normal;
} }
.expand { .expand-container {
display: block; display: flex;
} }
.actor-stash { .actor-stash {
@ -602,7 +666,7 @@ defineProps({
} }
} }
@media(max-width: $breakpoint0) { @media(--small-30) {
.header-social { .header-social {
display: none; display: none;
} }
@ -625,5 +689,23 @@ defineProps({
transform: translate(0, -.1rem); transform: translate(0, -.1rem);
} }
} }
*/
@media(--small-60) {
.birthdate-long,
.age {
display: none;
}
.birthdate-short {
display: inline-block;
}
.country-name {
display: none;
}
.country-alpha2 {
display: inline-block;
}
}
</style> </style>

View File

@ -35,6 +35,21 @@
<div class="content"> <div class="content">
<Bio :actor="actor" /> <Bio :actor="actor" />
<div class="photos nobar">
<img
v-for="photo in photos"
:key="`photo-${photo.id}`"
:src="getPath(photo, 'thumbnail')"
:width="photo.width"
:height="photo.height"
:style="{ 'background-image': `url('${getPath(photo, 'lazy')}')` }"
:title="photo.credit && `© ${photo.credit}`"
loading="lazy"
class="photo"
:class="{ avatar: photo.isAvatar }"
>
</div>
<Scenes /> <Scenes />
</div> </div>
</div> </div>
@ -43,6 +58,8 @@
<script setup> <script setup>
import { inject } from 'vue'; import { inject } from 'vue';
import getPath from '#/src/get-path.js';
import Bio from '#/components/actors/bio.vue'; import Bio from '#/components/actors/bio.vue';
import Gender from '#/components/actors/gender.vue'; import Gender from '#/components/actors/gender.vue';
import Scenes from '#/components/scenes/scenes.vue'; import Scenes from '#/components/scenes/scenes.vue';
@ -51,6 +68,14 @@ import Heart from '#/components/stashes/heart.vue';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const { pageProps } = pageContext; const { pageProps } = pageContext;
const { actor } = pageProps; const { actor } = pageProps;
const photos = [
actor.avatar && {
...actor.avatar,
isAvatar: true,
},
...actor.photos,
].filter(Boolean);
</script> </script>
<style scoped> <style scoped>
@ -81,12 +106,6 @@ const { actor } = pageProps;
flex-shrink: 0; flex-shrink: 0;
} }
.header-gender {
display: inline-block;
margin: 0 0 0 .5rem;
transform: translate(0, .125rem);
}
.header-social { .header-social {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
@ -103,4 +122,39 @@ const { actor } = pageProps;
.bookmarks { .bookmarks {
margin-right: .5rem; margin-right: .5rem;
} }
.photos {
display: flex;
gap: 0.5rem;
padding: .5rem;
border-bottom: solid 1px var(--shadow-weak-40);
background: var(--background-base-10);
overflow-x: auto;
}
.photo {
height: 14rem;
width: auto;
object-fit: cover;
object-position: 50% 0;
background-size: cover;
background-position: center;
box-shadow: 0 0px 3px var(--shadow-weak-30);
&.avatar {
display: none;
}
}
@media(--compact) {
.photo.avatar {
display: inline-block;
}
}
@media(--small) {
.photo {
height: 10rem;
}
}
</style> </style>

View File

@ -200,11 +200,11 @@ const scrollable = computed(() => children.value?.scrollWidth > children.value?.
align-items: center; align-items: center;
padding: .5rem; padding: .5rem;
border: none; border: none;
background: var(--grey-dark-40); background: var(--grey-dark-50);
color: var(--highlight-strong-30); color: var(--highlight-strong-30);
font-size: .9rem; font-size: .9rem;
font-weight: bold; font-weight: bold;
border-radius: .25rem; border-radius: .5rem;
box-shadow: 0 0 3px var(--shadow); box-shadow: 0 0 3px var(--shadow);
.icon { .icon {

View File

@ -58,8 +58,21 @@ export function curateActor(actor, context = {}) {
path: actor.avatar.path, path: actor.avatar.path,
thumbnail: actor.avatar.thumbnail, thumbnail: actor.avatar.thumbnail,
lazy: actor.avatar.lazy, lazy: actor.avatar.lazy,
width: actor.avatar.width,
height: actor.avatar.height,
isS3: actor.avatar.is_s3, isS3: actor.avatar.is_s3,
credit: actor.avatar.credit,
}, },
photos: context.photos?.map((photo) => ({
id: photo.id,
path: photo.path,
thumbnail: photo.thumbnail,
lazy: photo.lazy,
width: photo.width,
height: photo.height,
isS3: photo.is_s3,
credit: photo.credit,
})),
createdAt: actor.created_at, createdAt: actor.created_at,
updatedAt: actor.updated_at, updatedAt: actor.updated_at,
likes: actor.stashed, likes: actor.stashed,
@ -96,7 +109,7 @@ export function sortActorsByGender(actors, context = {}) {
} }
export async function fetchActorsById(actorIds, options = {}, reqUser) { export async function fetchActorsById(actorIds, options = {}, reqUser) {
const [actors, stashes] = await Promise.all([ const [actors, photos, stashes] = await Promise.all([
knex('actors') knex('actors')
.select( .select(
'actors.*', 'actors.*',
@ -115,6 +128,14 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
builder.orderBy(...options.order); builder.orderBy(...options.order);
} }
}), }),
knex('actors_profiles')
.select('actors_profiles.actor_id', 'media.*')
.leftJoin('actors', 'actors.id', 'actors_profiles.actor_id')
.leftJoin('media', 'media.id', 'actors_profiles.avatar_media_id')
.whereIn('actor_id', actorIds)
.whereNotNull('actors_profiles.avatar_media_id')
.whereNot('actors_profiles.avatar_media_id', knex.raw('actors.avatar_media_id')) // don't include main avatar as photo
.groupBy('actors_profiles.actor_id', 'media.id', 'media.hash'),
reqUser reqUser
? knex('stashes_actors') ? knex('stashes_actors')
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id') .leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
@ -140,6 +161,7 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
return curateActor(actor, { return curateActor(actor, {
stashes: stashes.filter((stash) => stash.actor_id === actor.id), stashes: stashes.filter((stash) => stash.actor_id === actor.id),
photos: photos.filter((photo) => photo.actor_id === actor.id),
append: options.append, append: options.append,
}); });
}).filter(Boolean); }).filter(Boolean);
@ -160,173 +182,6 @@ function curateOptions(options) {
}; };
} }
/*
const sortMap = {
likes: 'stashed',
scenes: 'scenes',
relevance: '_score',
};
function getSort(order) {
if (order[0] === 'name') {
return [{
slug: order[1],
}];
}
return [
{
[sortMap[order[0]]]: order[1],
},
{
slug: 'asc', // sort by name where primary order is equal
},
];
}
function buildQuery(filters) {
const query = {
bool: {
must: [],
},
};
const expressions = {
age: 'if(date_of_birth, floor((now() - date_of_birth) / 31556952), 0)',
};
if (filters.query) {
query.bool.must.push({
match: {
name: filters.query,
},
});
}
['gender', 'country'].forEach((attribute) => {
if (filters[attribute]) {
query.bool.must.push({
equals: {
[attribute]: filters[attribute],
},
});
}
});
['age', 'height', 'weight'].forEach((attribute) => {
if (filters[attribute]) {
query.bool.must.push({
range: {
[attribute]: {
gte: filters[attribute][0],
lte: filters[attribute][1],
},
},
});
}
});
if (filters.dateOfBirth && filters.dobType === 'dateOfBirth') {
query.bool.must.push({
equals: {
date_of_birth: Math.floor(filters.dateOfBirth.getTime() / 1000),
},
});
}
if (filters.dateOfBirth && filters.dobType === 'birthday') {
expressions.month_of_birth = 'month(date_of_birth)';
expressions.day_of_birth = 'day(date_of_birth)';
const month = filters.dateOfBirth.getMonth() + 1;
const day = filters.dateOfBirth.getDate();
query.bool.must.push({
bool: {
must: [
{
equals: {
month_of_birth: month,
},
},
{
equals: {
day_of_birth: day,
},
},
],
},
});
}
if (filters.cup) {
expressions.cup_in_range = `regex(cup, '^[${filters.cup[0]}-${filters.cup[1]}]')`;
query.bool.must.push({
equals: {
cup_in_range: 1,
},
});
}
if (typeof filters.naturalBoobs === 'boolean') {
query.bool.must.push({
equals: {
natural_boobs: filters.naturalBoobs ? 2 : 1, // manticore boolean does not support null, so 0 = null, 1 = false (enhanced), 2 = true (natural)
},
});
}
if (filters.requireAvatar) {
query.bool.must.push({
equals: {
has_avatar: 1,
},
});
}
return { query, expressions };
}
async function queryManticoreJson(filters, options) {
const { query, expressions } = buildQuery(filters);
const result = await searchApi.search({
index: 'actors',
query,
expressions,
limit: options.limit,
offset: (options.page - 1) * options.limit,
sort: getSort(options.order, filters),
aggs: {
countries: {
terms: {
field: 'country',
size: 300,
},
sort: [{ country: { order: 'asc' } }],
},
},
options: {
max_matches: config.database.manticore.maxMatches,
max_query_time: config.database.manticore.maxQueryTime,
},
});
const actors = result.hits.hits.map((hit) => ({
id: hit._id,
...hit._source,
_score: hit._score,
}));
return {
actors,
total: result.hits.total,
aggregations: result.aggregations && Object.fromEntries(Object.entries(result.aggregations).map(([key, { buckets }]) => [key, buckets])),
};
}
*/
async function queryManticoreSql(filters, options, _reqUser) { async function queryManticoreSql(filters, options, _reqUser) {
const aggSize = config.database.manticore.maxAggregateSize; const aggSize = config.database.manticore.maxAggregateSize;