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-30 (max-width: 540px);
@custom-media --small-20 (max-width: 650px);
@custom-media --small-15 (max-width: 720px);
@custom-media --small-10 (max-width: 768px);
@custom-media --small (max-width: 900px);
@custom-media --compact (max-width: 1200px);

View File

@ -1,25 +1,18 @@
<template>
<div
class="content-inner actor-inner"
@scroll="events.emit('scroll', $event)"
>
<div
class="profile"
:class="{ expanded, 'with-avatar': !!actor.avatar }"
>
<a
<div
v-if="actor.avatar"
:href="getMediaPath(actor.avatar)"
target="_blank"
rel="noopener noreferrer"
class="avatar-link"
class="avatar-container"
>
<img
:src="getMediaPath(actor.avatar, 'thumbnail')"
:title="actor.avatar.credit && `© ${actor.avatar.credit}`"
class="avatar"
>
</a>
</div>
<ul class="bio nolist">
<li
@ -28,10 +21,15 @@
>
<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"
class="age"
>{{ actor.ageFromBirth }}</span></span>
>{{ actor.ageFromBirth }}</span>
</span>
</li>
<li
@ -89,7 +87,10 @@
<img
class="flag"
: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>
</li>
@ -238,7 +239,7 @@
<span v-else>Yes</span>
</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>
<div class="descriptions-container">
@ -268,6 +269,23 @@
</p>
</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>
</template>
@ -278,7 +296,7 @@ import { ref } from 'vue';
import { getMediaPath } from '#/utils/media-path.js';
import { formatDate } from '#/utils/format.js';
const expanded = ref(true);
const expanded = ref(false);
defineProps({
actor: {
@ -289,10 +307,18 @@ defineProps({
</script>
<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;
height: 1.25rem;
}
}
</style>
<style scoped>
@ -304,12 +330,13 @@ defineProps({
display: flex;
flex-direction: row;
flex-shrink: 0;
position: relative;
&.with-avatar {
height: 18rem; /* profile overlaps avatar in chrome */
}
.avatar-link {
.avatar-container {
padding: 0 0 1rem 1rem;
flex-shrink: 0;
}
@ -320,6 +347,11 @@ defineProps({
border: solid 3px var(--highlight-hint);
margin: 0 .5rem 0 0;
}
&.expanded {
padding-bottom: 1.5rem;
margin-bottom: .75rem;
}
}
.bio {
@ -404,10 +436,14 @@ defineProps({
display: block;
}
.birthdate-short {
display: none;
}
.age {
font-weight: bold;
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;
}
@ -416,6 +452,10 @@ defineProps({
justify-content: flex-end;
}
.country-alpha2 {
display: none;
}
.figure .bio-label .icon {
margin: -.5rem .5rem 0 0;
}
@ -451,8 +491,8 @@ defineProps({
content: ',\00a0';
}
.scraped {
color: var(--highlight-weak);
.updated {
color: var(--highlight-weak-20);
font-size: .8rem;
}
@ -519,8 +559,33 @@ defineProps({
display: none;
}
.expand {
.expand-container {
width: 100%;
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 {
@ -544,15 +609,14 @@ defineProps({
}
}
/*
@media(max-width: $breakpoint4) {
@media(--big) {
.descriptions-container {
display: none;
}
}
@media(max-width: $breakpoint3) {
.profile .avatar-link {
@media(--compact) {
.profile .avatar-container {
display: none;
}
@ -561,7 +625,7 @@ defineProps({
}
}
@media(max-width: $breakpoint) {
@media(--small-15) {
.profile {
height: auto;
max-height: none;
@ -593,8 +657,8 @@ defineProps({
white-space: normal;
}
.expand {
display: block;
.expand-container {
display: flex;
}
.actor-stash {
@ -602,7 +666,7 @@ defineProps({
}
}
@media(max-width: $breakpoint0) {
@media(--small-30) {
.header-social {
display: none;
}
@ -625,5 +689,23 @@ defineProps({
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>

View File

@ -35,6 +35,21 @@
<div class="content">
<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 />
</div>
</div>
@ -43,6 +58,8 @@
<script setup>
import { inject } from 'vue';
import getPath from '#/src/get-path.js';
import Bio from '#/components/actors/bio.vue';
import Gender from '#/components/actors/gender.vue';
import Scenes from '#/components/scenes/scenes.vue';
@ -51,6 +68,14 @@ import Heart from '#/components/stashes/heart.vue';
const pageContext = inject('pageContext');
const { pageProps } = pageContext;
const { actor } = pageProps;
const photos = [
actor.avatar && {
...actor.avatar,
isAvatar: true,
},
...actor.photos,
].filter(Boolean);
</script>
<style scoped>
@ -81,12 +106,6 @@ const { actor } = pageProps;
flex-shrink: 0;
}
.header-gender {
display: inline-block;
margin: 0 0 0 .5rem;
transform: translate(0, .125rem);
}
.header-social {
overflow: hidden;
white-space: nowrap;
@ -103,4 +122,39 @@ const { actor } = pageProps;
.bookmarks {
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>

View File

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

View File

@ -58,8 +58,21 @@ export function curateActor(actor, context = {}) {
path: actor.avatar.path,
thumbnail: actor.avatar.thumbnail,
lazy: actor.avatar.lazy,
width: actor.avatar.width,
height: actor.avatar.height,
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,
updatedAt: actor.updated_at,
likes: actor.stashed,
@ -96,7 +109,7 @@ export function sortActorsByGender(actors, context = {}) {
}
export async function fetchActorsById(actorIds, options = {}, reqUser) {
const [actors, stashes] = await Promise.all([
const [actors, photos, stashes] = await Promise.all([
knex('actors')
.select(
'actors.*',
@ -115,6 +128,14 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
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
? knex('stashes_actors')
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
@ -140,6 +161,7 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
return curateActor(actor, {
stashes: stashes.filter((stash) => stash.actor_id === actor.id),
photos: photos.filter((photo) => photo.actor_id === actor.id),
append: options.append,
});
}).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) {
const aggSize = config.database.manticore.maxAggregateSize;