Added actor stash.
This commit is contained in:
parent
9b50b53df6
commit
a8aab600c7
|
@ -192,3 +192,9 @@
|
||||||
.v-popper--theme-dropdown .v-popper__arrow-outer {
|
.v-popper--theme-dropdown .v-popper__arrow-outer {
|
||||||
border-color: #ddd;
|
border-color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resize-observer {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
|
@ -1,30 +1,297 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<Filters :results="total">
|
||||||
|
<div class="filter">
|
||||||
|
<input
|
||||||
|
v-model="q"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search actors"
|
||||||
|
class="input search"
|
||||||
|
@search="search"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GenderFilter
|
||||||
|
:filters="filters"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BirthdateFilter
|
||||||
|
:filters="filters"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<BoobsFilter
|
||||||
|
:filters="filters"
|
||||||
|
:cup-range="cupRange"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PhysiqueFilter
|
||||||
|
:filters="filters"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CountryFilter
|
||||||
|
:filters="filters"
|
||||||
|
:countries="countries"
|
||||||
|
@update="updateFilter"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="filter">
|
||||||
|
<Checkbox
|
||||||
|
:checked="filters.avatarRequired"
|
||||||
|
label="Require photo"
|
||||||
|
@change="(checked) => updateFilter('avatarRequired', checked, true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Filters>
|
||||||
|
|
||||||
|
<div class="actors-container">
|
||||||
|
<div class="actors-header">
|
||||||
|
<div class="meta">
|
||||||
|
<span class="count">{{ total }} results</span>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="order"
|
||||||
|
class="input"
|
||||||
|
@change="search({ autoScope: false })"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-if="pageStash"
|
||||||
|
:selected="order === 'stashed.desc'"
|
||||||
|
value="stashed.desc"
|
||||||
|
>Added</option>
|
||||||
|
|
||||||
|
<option
|
||||||
|
v-if="q"
|
||||||
|
:selected="order === 'relevance.desc'"
|
||||||
|
value="relevance.desc"
|
||||||
|
>Relevance</option>
|
||||||
|
|
||||||
|
<option value="name.asc">Name</option>
|
||||||
|
<option value="likes.desc">Likes</option>
|
||||||
|
<option value="scenes.desc">Scenes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="actors nolist">
|
<ul class="actors nolist">
|
||||||
<li
|
<li
|
||||||
v-for="actor in actors"
|
v-for="actor in actors"
|
||||||
:key="`actor-${actor.id}`"
|
:key="`actor-${actor.id}`"
|
||||||
class="actor"
|
|
||||||
>
|
>
|
||||||
<ActorTile :actor="actor" />
|
<ActorTile
|
||||||
|
:actor="actor"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
:total="total"
|
||||||
|
:redirect="false"
|
||||||
|
@navigation="paginate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ActorTile from './tile.vue';
|
import { ref, inject } from 'vue';
|
||||||
|
import { format, subYears } from 'date-fns';
|
||||||
|
import { parse } from 'path-to-regexp';
|
||||||
|
|
||||||
defineProps({
|
import navigate from '#/src/navigate.js';
|
||||||
actors: {
|
import { get } from '#/src/api.js';
|
||||||
type: Array,
|
import events from '#/src/events.js';
|
||||||
default: () => [],
|
|
||||||
},
|
import ActorTile from '#/components/actors/tile.vue';
|
||||||
|
import Pagination from '#/components/pagination/pagination.vue';
|
||||||
|
import Checkbox from '#/components/form/checkbox.vue';
|
||||||
|
import Filters from '#/components/filters/filters.vue';
|
||||||
|
import GenderFilter from '#/components/filters/gender.vue';
|
||||||
|
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
||||||
|
import BoobsFilter from '#/components/filters/boobs.vue';
|
||||||
|
import PhysiqueFilter from '#/components/filters/physique.vue';
|
||||||
|
import CountryFilter from '#/components/filters/country.vue';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { pageProps, urlParsed, routeParams } = pageContext;
|
||||||
|
|
||||||
|
const q = ref(urlParsed.search.q);
|
||||||
|
const actors = ref([]);
|
||||||
|
const pageStash = pageProps.stash;
|
||||||
|
const countries = ref(pageProps.countries);
|
||||||
|
const cupRange = ref(pageProps.cupRange);
|
||||||
|
|
||||||
|
actors.value = pageProps.actors;
|
||||||
|
|
||||||
|
const currentPage = ref(Number(routeParams.page));
|
||||||
|
const total = ref(Number(pageProps.total));
|
||||||
|
const order = ref(routeParams.order || urlParsed.search.order || 'likes.desc');
|
||||||
|
|
||||||
|
const filters = ref({
|
||||||
|
gender: urlParsed.search.gender,
|
||||||
|
ageRequired: !!urlParsed.search.age,
|
||||||
|
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
||||||
|
dobRequired: !!urlParsed.search.dob,
|
||||||
|
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
||||||
|
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
||||||
|
country: urlParsed.search.c,
|
||||||
|
braSizeRequired: !!urlParsed.search.cup,
|
||||||
|
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
||||||
|
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
||||||
|
heightRequired: !!urlParsed.search.height,
|
||||||
|
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
||||||
|
weightRequired: !!urlParsed.search.weight,
|
||||||
|
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
||||||
|
avatarRequired: !!urlParsed.search.avatar,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getPath(preserveQuery) {
|
||||||
|
const path = parse(routeParams.path).map((segment) => {
|
||||||
|
if (segment.name === 'page') {
|
||||||
|
return `${segment.prefix}${1}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${segment.prefix || ''}${routeParams[segment.name] || segment}`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
if (preserveQuery && urlParsed.searchOriginal) {
|
||||||
|
return `${path}${urlParsed.searchOriginal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(options = {}) {
|
||||||
|
if (options.resetPage !== false) {
|
||||||
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.autoScope !== false) {
|
||||||
|
if (q.value) {
|
||||||
|
order.value = 'relevance.desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!q.value && order.value.includes('relevance')) {
|
||||||
|
order.value = 'likes.desc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
q: q.value || undefined,
|
||||||
|
order: order.value,
|
||||||
|
gender: filters.value.gender || undefined,
|
||||||
|
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
||||||
|
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
||||||
|
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
||||||
|
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
||||||
|
c: filters.value.country || undefined,
|
||||||
|
nb: filters.value.naturalBoobs,
|
||||||
|
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
||||||
|
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
||||||
|
avatar: filters.value.avatarRequired || undefined,
|
||||||
|
stashId: pageStash?.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
navigate(getPath(false), query, { redirect: false });
|
||||||
|
|
||||||
|
const res = await get('/actors', {
|
||||||
|
...query,
|
||||||
|
page: currentPage.value, // client uses param rather than query pagination
|
||||||
|
});
|
||||||
|
|
||||||
|
actors.value = res.actors;
|
||||||
|
total.value = res.total;
|
||||||
|
|
||||||
|
countries.value = res.countries;
|
||||||
|
|
||||||
|
events.emit('scrollUp');
|
||||||
|
}
|
||||||
|
|
||||||
|
function paginate({ page }) {
|
||||||
|
currentPage.value = page;
|
||||||
|
|
||||||
|
search({ resetPage: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilter(prop, value, reload = true) {
|
||||||
|
filters.value[prop] = value;
|
||||||
|
|
||||||
|
if (reload) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.actors {
|
.gender-button {
|
||||||
display: grid;
|
&.selected .gender .icon {
|
||||||
grid-template-columns: repeat(auto-fill, 10rem);
|
fill: var(--text-light);
|
||||||
gap: .5rem;
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.selected) {
|
||||||
|
.gender .icon {
|
||||||
|
fill: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.male .icon {
|
||||||
|
filter: drop-shadow(0 0 1px var(--male));
|
||||||
|
}
|
||||||
|
|
||||||
|
.female .icon {
|
||||||
|
filter: drop-shadow(0 0 1px var(--female));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.selected) .transsexual .icon {
|
||||||
|
fill: var(--female);
|
||||||
|
filter: drop-shadow(1px 0 0 var(--text-light)) drop-shadow(-1px 0 0 var(--text-light)) drop-shadow(0 1px 0 var(--text-light)) drop-shadow(0 -1px 0 var(--text-light)) drop-shadow(1px 0 0 var(--male)) drop-shadow(-1px 0 0 var(--male)) drop-shadow(0 1px 0 var(--male)) drop-shadow(0 -1px 0 var(--male)) drop-shadow(0 0 1px rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actors-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5rem 0 .25rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actors-container {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 1rem 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actors {
|
||||||
|
display: grid;
|
||||||
|
flex-grow: 1;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||||
|
gap: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(--small-40) {
|
||||||
|
.actors {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="actor">
|
<div
|
||||||
|
class="tile"
|
||||||
|
:class="{
|
||||||
|
unstashed: !favorited && pageStash && user && pageStash.id === user.primaryStash?.id
|
||||||
|
}"
|
||||||
|
>
|
||||||
<span class="name">{{ actor.name }}</span>
|
<span class="name">{{ actor.name }}</span>
|
||||||
|
|
||||||
|
<div class="avatar-container">
|
||||||
<Link
|
<Link
|
||||||
:href="`/actor/${actor.id}/${actor.slug}`"
|
:href="`/actor/${actor.id}/${actor.slug}`"
|
||||||
class="avatar-link no-link"
|
class="avatar-link no-link"
|
||||||
|
@ -15,6 +21,21 @@
|
||||||
>
|
>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-show="favorited"
|
||||||
|
icon="heart7"
|
||||||
|
class="heart favorited"
|
||||||
|
@click.native.stop="unstash"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-show="!favorited && user"
|
||||||
|
icon="heart8"
|
||||||
|
class="heart"
|
||||||
|
@click.native.stop="stash"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<span class="birth">
|
<span class="birth">
|
||||||
<Gender :gender="actor.gender" />
|
<Gender :gender="actor.gender" />
|
||||||
|
@ -48,20 +69,72 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, inject } from 'vue';
|
||||||
|
|
||||||
import { formatDate } from '#/utils/format.js';
|
import { formatDate } from '#/utils/format.js';
|
||||||
|
import { post, del } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
import Gender from './gender.vue';
|
import Gender from './gender.vue';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
actor: {
|
actor: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { user } = pageContext;
|
||||||
|
const pageStash = pageContext.pageProps.stash;
|
||||||
|
|
||||||
|
// console.log(props.actor);
|
||||||
|
|
||||||
|
const favorited = ref(props.actor.stashes?.some((sceneStash) => sceneStash.primary) || false);
|
||||||
|
|
||||||
|
async function stash() {
|
||||||
|
try {
|
||||||
|
favorited.value = true;
|
||||||
|
await post(`/stashes/${user.primaryStash.id}/actors`, { actorId: props.actor.id });
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: `${props.actor.name} stashed to ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
favorited.value = false;
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to stash ${props.actor.name} to ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unstash() {
|
||||||
|
try {
|
||||||
|
favorited.value = false;
|
||||||
|
await del(`/stashes/${user.primaryStash.id}/actors/${props.actor.id}`);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'remove',
|
||||||
|
message: `${props.actor.name} unstashed from ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
favorited.value = true;
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to unstash ${props.actor.name} from ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.actor {
|
.tile {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -79,6 +152,10 @@ defineProps({
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.unstashed {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
@ -92,9 +169,14 @@ defineProps({
|
||||||
user-select: all;
|
user-select: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-link {
|
.avatar-container {
|
||||||
display: block;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-link {
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +189,26 @@ defineProps({
|
||||||
background-position: center 0;
|
background-position: center 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon.heart {
|
||||||
|
width: 2rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: .5rem .5rem 1rem 1rem;
|
||||||
|
fill: var(--highlight-strong-10);
|
||||||
|
filter: drop-shadow(0 0 3px var(--shadow));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.favorited {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
|
|
|
@ -103,7 +103,7 @@
|
||||||
:href="`/user/${user.username}`"
|
:href="`/user/${user.username}`"
|
||||||
class="menu-button nolink"
|
class="menu-button nolink"
|
||||||
>
|
>
|
||||||
<Icon icon="vcard" />
|
<Icon icon="user7" />
|
||||||
View profile
|
View profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -332,7 +332,7 @@ async function logout() {
|
||||||
.menu-button {
|
.menu-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: .5rem .5rem .5rem .75rem;
|
padding: .75rem .5rem .75rem .75rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -342,8 +342,16 @@ async function logout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--shadow-weak-30);
|
color: var(--primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:not(.logout) .icon {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.logout {
|
||||||
|
color: var(--error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
<option value="latest">Latest</option>
|
<option value="latest">Latest</option>
|
||||||
<option value="upcoming">Upcoming</option>
|
<option value="upcoming">Upcoming</option>
|
||||||
<option value="new">New</option>
|
<option value="new">New</option>
|
||||||
<option value="likes">Likes</option>
|
<option value="likes">Popular</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,8 @@ import { ref, inject } from 'vue';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
import { post, del } from '#/src/api.js';
|
import { post, del } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
import ellipsis from '#/utils/ellipsis.js';
|
||||||
|
|
||||||
import Icon from '../icon/icon.vue';
|
import Icon from '../icon/icon.vue';
|
||||||
|
|
||||||
|
@ -125,23 +127,47 @@ const user = pageContext.user;
|
||||||
const pageStash = pageContext.pageProps.stash;
|
const pageStash = pageContext.pageProps.stash;
|
||||||
|
|
||||||
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.primary));
|
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.primary));
|
||||||
|
const fbCutoff = 20;
|
||||||
|
|
||||||
async function stash() {
|
async function stash() {
|
||||||
try {
|
try {
|
||||||
favorited.value = true;
|
favorited.value = true;
|
||||||
|
|
||||||
await post(`/stashes/${user.primaryStash.id}/scenes`, { sceneId: props.scene.id });
|
await post(`/stashes/${user.primaryStash.id}/scenes`, { sceneId: props.scene.id });
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: `"${ellipsis(props.scene.title, fbCutoff)}" stashed to ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
favorited.value = false;
|
favorited.value = false;
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to stash "${ellipsis(props.scene.title, fbCutoff)}" to ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unstash() {
|
async function unstash() {
|
||||||
try {
|
try {
|
||||||
favorited.value = false;
|
favorited.value = false;
|
||||||
|
|
||||||
await del(`/stashes/${user.primaryStash.id}/scenes/${props.scene.id}`);
|
await del(`/stashes/${user.primaryStash.id}/scenes/${props.scene.id}`);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'remove',
|
||||||
|
message: `"${ellipsis(props.scene.title, fbCutoff)}" unstashed from ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
|
||||||
favorited.value = true;
|
favorited.value = true;
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to unstash "${ellipsis(props.scene.title, fbCutoff)}" from ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -26,8 +26,28 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scenes-container">
|
<div class="content">
|
||||||
<Scenes />
|
<nav class="domains">
|
||||||
|
<Link
|
||||||
|
:href="getPath('scenes')"
|
||||||
|
class="domain nolink"
|
||||||
|
:active="domain === 'scenes'"
|
||||||
|
>Scenes</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
:href="getPath('actors')"
|
||||||
|
class="domain nolink"
|
||||||
|
:active="domain === 'actors'"
|
||||||
|
>Actors</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
:href="getPath('movies')"
|
||||||
|
class="domain nolink"
|
||||||
|
:active="domain === 'movies'"
|
||||||
|
>Movies</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -35,10 +55,15 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
|
|
||||||
import Scenes from '#/components/scenes/scenes.vue';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
|
const { routeParams } = pageContext;
|
||||||
|
|
||||||
|
const domain = routeParams.domain;
|
||||||
const stash = pageContext.pageProps.stash;
|
const stash = pageContext.pageProps.stash;
|
||||||
|
|
||||||
|
function getPath(targetDomain) {
|
||||||
|
return `/stash/${stash.user.username}/${stash.slug}/${targetDomain}`;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -91,6 +116,31 @@ const stash = pageContext.pageProps.stash;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.domains {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
background: var(--background-dark-20);
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: var(--shadow);
|
||||||
|
font-size: .9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media(--small-50) {
|
@media(--small-50) {
|
||||||
.title {
|
.title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
|
@ -73,6 +73,9 @@ module.exports = {
|
||||||
usernameLength: [2, 24],
|
usernameLength: [2, 24],
|
||||||
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
||||||
},
|
},
|
||||||
|
stashes: {
|
||||||
|
viewRefreshCooldown: 60, // minutes
|
||||||
|
},
|
||||||
exclude: {
|
exclude: {
|
||||||
channels: [
|
channels: [
|
||||||
// 21sextreme, no longer updated
|
// 21sextreme, no longer updated
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default {
|
||||||
|
meta: {
|
||||||
|
data: {
|
||||||
|
env: {
|
||||||
|
server: true,
|
||||||
|
client: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function data() {
|
||||||
|
return {
|
||||||
|
foo: 'bar',
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,270 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div>
|
||||||
<Filters :results="total">
|
<Actors />
|
||||||
<div class="filter">
|
|
||||||
<input
|
|
||||||
v-model="q"
|
|
||||||
type="search"
|
|
||||||
placeholder="Search actors"
|
|
||||||
class="input search"
|
|
||||||
@search="search"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GenderFilter
|
|
||||||
:filters="filters"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BirthdateFilter
|
|
||||||
:filters="filters"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<BoobsFilter
|
|
||||||
:filters="filters"
|
|
||||||
:cup-range="cupRange"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PhysiqueFilter
|
|
||||||
:filters="filters"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CountryFilter
|
|
||||||
:filters="filters"
|
|
||||||
:countries="countries"
|
|
||||||
@update="updateFilter"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="filter">
|
|
||||||
<Checkbox
|
|
||||||
:checked="filters.avatarRequired"
|
|
||||||
label="Require photo"
|
|
||||||
@change="(checked) => updateFilter('avatarRequired', checked, true)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Filters>
|
|
||||||
|
|
||||||
<div class="actors-container">
|
|
||||||
<div class="actors-header">
|
|
||||||
<div class="meta">
|
|
||||||
<span class="count">{{ total }} results</span>
|
|
||||||
|
|
||||||
<select
|
|
||||||
v-model="order"
|
|
||||||
class="input"
|
|
||||||
@change="search({ autoScope: false })"
|
|
||||||
>
|
|
||||||
<option value="name.asc">Name</option>
|
|
||||||
<option value="likes.desc">Likes</option>
|
|
||||||
<option value="scenes.desc">Scenes</option>
|
|
||||||
<option
|
|
||||||
value="relevance.desc"
|
|
||||||
:disabled="!q"
|
|
||||||
>Relevance</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="actors nolist">
|
|
||||||
<li
|
|
||||||
v-for="actor in actors"
|
|
||||||
:key="`actor-${actor.id}`"
|
|
||||||
>
|
|
||||||
<ActorTile
|
|
||||||
:actor="actor"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<Pagination
|
|
||||||
:page="currentPage"
|
|
||||||
:total="total"
|
|
||||||
:redirect="false"
|
|
||||||
@navigation="paginate"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject } from 'vue';
|
import Actors from '#/components/actors/actors.vue';
|
||||||
import { format, subYears } from 'date-fns';
|
|
||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
|
||||||
import { get } from '#/src/api.js';
|
|
||||||
import events from '#/src/events.js';
|
|
||||||
|
|
||||||
import ActorTile from '#/components/actors/tile.vue';
|
|
||||||
import Pagination from '#/components/pagination/pagination.vue';
|
|
||||||
import Checkbox from '#/components/form/checkbox.vue';
|
|
||||||
import Filters from '#/components/filters/filters.vue';
|
|
||||||
import GenderFilter from '#/components/filters/gender.vue';
|
|
||||||
import BirthdateFilter from '#/components/filters/birthdate.vue';
|
|
||||||
import BoobsFilter from '#/components/filters/boobs.vue';
|
|
||||||
import PhysiqueFilter from '#/components/filters/physique.vue';
|
|
||||||
import CountryFilter from '#/components/filters/country.vue';
|
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
|
||||||
const { pageProps, urlParsed, routeParams } = pageContext;
|
|
||||||
|
|
||||||
const q = ref(urlParsed.search.q);
|
|
||||||
const actors = ref([]);
|
|
||||||
const countries = ref(pageProps.countries);
|
|
||||||
const cupRange = ref(pageProps.cupRange);
|
|
||||||
|
|
||||||
actors.value = pageProps.actors;
|
|
||||||
|
|
||||||
const currentPage = ref(Number(routeParams.page));
|
|
||||||
const total = ref(Number(pageProps.total));
|
|
||||||
const order = ref(urlParsed.search.order || 'likes.desc');
|
|
||||||
|
|
||||||
const filters = ref({
|
|
||||||
gender: urlParsed.search.gender,
|
|
||||||
ageRequired: !!urlParsed.search.age,
|
|
||||||
age: urlParsed.search.age?.split(',').map((age) => Number(age)) || [18, 100],
|
|
||||||
dobRequired: !!urlParsed.search.dob,
|
|
||||||
dobType: urlParsed.search.dobt ? ({ bd: 'birthday', dob: 'dob' })[urlParsed.search.dobt] : 'birthday',
|
|
||||||
dob: urlParsed.search.dob || format(subYears(new Date(), 21), 'yyyy-MM-dd'),
|
|
||||||
country: urlParsed.search.c,
|
|
||||||
braSizeRequired: !!urlParsed.search.cup,
|
|
||||||
braSize: urlParsed.search.cup?.split(',') || ['A', 'Z'],
|
|
||||||
naturalBoobs: urlParsed.search.nb ? urlParsed.search.nb === 'true' : undefined,
|
|
||||||
heightRequired: !!urlParsed.search.height,
|
|
||||||
height: urlParsed.search.height?.split(',').map((height) => Number(height)) || [50, 220],
|
|
||||||
weightRequired: !!urlParsed.search.weight,
|
|
||||||
weight: urlParsed.search.weight?.split(',').map((weight) => Number(weight)) || [30, 200],
|
|
||||||
avatarRequired: !!urlParsed.search.avatar,
|
|
||||||
});
|
|
||||||
|
|
||||||
async function search(options = {}) {
|
|
||||||
if (options.resetPage !== false) {
|
|
||||||
currentPage.value = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.autoScope !== false) {
|
|
||||||
if (q.value) {
|
|
||||||
order.value = 'relevance.desc';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!q.value && order.value.includes('relevance')) {
|
|
||||||
order.value = 'likes.desc';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
q: q.value || undefined,
|
|
||||||
order: order.value,
|
|
||||||
gender: filters.value.gender || undefined,
|
|
||||||
age: filters.value.ageRequired ? filters.value.age.join(',') : undefined,
|
|
||||||
dob: filters.value.dobRequired ? filters.value.dob : undefined,
|
|
||||||
dobt: filters.value.dobRequired ? ({ birthday: 'bd', dob: 'dob' })[filters.value.dobType] : undefined,
|
|
||||||
cup: filters.value.braSizeRequired ? filters.value.braSize.join(',') : undefined,
|
|
||||||
c: filters.value.country || undefined,
|
|
||||||
nb: filters.value.naturalBoobs,
|
|
||||||
height: filters.value.heightRequired ? filters.value.height.join(',') : undefined,
|
|
||||||
weight: filters.value.weightRequired ? filters.value.weight.join(',') : undefined,
|
|
||||||
avatar: filters.value.avatarRequired || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
navigate(`/actors/${currentPage.value}`, query, { redirect: false });
|
|
||||||
|
|
||||||
const res = await get('/actors', {
|
|
||||||
...query,
|
|
||||||
page: currentPage.value, // client uses param rather than query pagination
|
|
||||||
});
|
|
||||||
|
|
||||||
actors.value = res.actors;
|
|
||||||
total.value = res.total;
|
|
||||||
|
|
||||||
countries.value = res.countries;
|
|
||||||
|
|
||||||
events.emit('scrollUp');
|
|
||||||
}
|
|
||||||
|
|
||||||
function paginate({ page }) {
|
|
||||||
currentPage.value = page;
|
|
||||||
|
|
||||||
search({ resetPage: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFilter(prop, value, reload = true) {
|
|
||||||
filters.value[prop] = value;
|
|
||||||
|
|
||||||
if (reload) {
|
|
||||||
search();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.gender-button {
|
|
||||||
&.selected .gender .icon {
|
|
||||||
fill: var(--text-light);
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(.selected) {
|
|
||||||
.gender .icon {
|
|
||||||
fill: var(--text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
.male .icon {
|
|
||||||
filter: drop-shadow(0 0 1px var(--male));
|
|
||||||
}
|
|
||||||
|
|
||||||
.female .icon {
|
|
||||||
filter: drop-shadow(0 0 1px var(--female));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not(.selected) .transsexual .icon {
|
|
||||||
fill: var(--female);
|
|
||||||
filter: drop-shadow(1px 0 0 var(--text-light)) drop-shadow(-1px 0 0 var(--text-light)) drop-shadow(0 1px 0 var(--text-light)) drop-shadow(0 -1px 0 var(--text-light)) drop-shadow(1px 0 0 var(--male)) drop-shadow(-1px 0 0 var(--male)) drop-shadow(0 1px 0 var(--male)) drop-shadow(0 -1px 0 var(--male)) drop-shadow(0 0 1px rgba(0, 0, 0, 0.5));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page {
|
|
||||||
min-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actors-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: .5rem 0 .25rem 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actors-container {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 1rem 1rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actors {
|
|
||||||
display: grid;
|
|
||||||
flex-grow: 1;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
|
||||||
gap: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(--small-40) {
|
|
||||||
.actors {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ export async function onBeforeRender(pageContext) {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 120,
|
limit: Number(pageContext.urlParsed.search.limit) || 120,
|
||||||
order: pageContext.urlParsed.search.order?.split('.') || ['likes', 'desc'],
|
order: pageContext.urlParsed.search.order?.split('.') || ['likes', 'desc'],
|
||||||
});
|
}, pageContext.user);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
|
|
|
@ -25,20 +25,19 @@
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!--
|
<Icon
|
||||||
<Social
|
v-show="favorited"
|
||||||
v-if="actor.social && actor.social.length > 0"
|
icon="heart7"
|
||||||
:actor="actor"
|
class="heart favorited"
|
||||||
class="header-social"
|
@click.native.stop="unstash"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StashButton
|
<Icon
|
||||||
:stashed-by="stashedBy"
|
v-show="!favorited && user"
|
||||||
class="actor-stash light"
|
icon="heart8"
|
||||||
@stash="(stash) => stashActor(stash)"
|
class="heart"
|
||||||
@unstash="(stash) => unstashActor(stash)"
|
@click.native.stop="stash"
|
||||||
/>
|
/>
|
||||||
-->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
@ -50,15 +49,61 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { ref, inject } from 'vue';
|
||||||
|
|
||||||
|
import { post, del } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.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';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const { pageProps } = pageContext;
|
const { pageProps, user } = pageContext;
|
||||||
const { actor } = pageProps;
|
const { actor } = pageProps;
|
||||||
|
|
||||||
|
const favorited = ref(actor.stashes?.some((sceneStash) => sceneStash.primary) || false);
|
||||||
|
|
||||||
|
async function stash() {
|
||||||
|
try {
|
||||||
|
favorited.value = true;
|
||||||
|
|
||||||
|
await post(`/stashes/${user.primaryStash.id}/actors`, { actorId: actor.id });
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: `${actor.name} stashed to ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
favorited.value = false;
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to stash ${actor.name} to ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unstash() {
|
||||||
|
try {
|
||||||
|
favorited.value = false;
|
||||||
|
await del(`/stashes/${user.primaryStash.id}/actors/${actor.id}`);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'remove',
|
||||||
|
message: `${actor.name} unstashed from ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
favorited.value = true;
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'error',
|
||||||
|
message: `Failed to unstash ${actor.name} from ${user.primaryStash.name}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -73,6 +118,7 @@ const { actor } = pageProps;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
color: var(--highlight-strong-30);
|
color: var(--highlight-strong-30);
|
||||||
background: var(--grey-dark-40);
|
background: var(--grey-dark-40);
|
||||||
}
|
}
|
||||||
|
@ -103,4 +149,24 @@ const { actor } = pageProps;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon.heart {
|
||||||
|
width: 2rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: .5rem 1rem 1rem 1rem;
|
||||||
|
fill: var(--highlight-strong-10);
|
||||||
|
filter: drop-shadow(0 0 3px var(--shadow));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.favorited {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
const [[actor], actorScenes] = await Promise.all([
|
const [[actor], actorScenes] = await Promise.all([
|
||||||
fetchActorsById([Number(pageContext.routeParams.actorId)]),
|
fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user),
|
||||||
fetchScenes(await curateScenesQuery({
|
fetchScenes(await curateScenesQuery({
|
||||||
...pageContext.urlQuery,
|
...pageContext.urlQuery,
|
||||||
scope: pageContext.routeParams.scope || 'latest',
|
scope: pageContext.routeParams.scope || 'latest',
|
||||||
|
@ -13,7 +13,7 @@ export async function onBeforeRender(pageContext) {
|
||||||
page: Number(pageContext.routeParams.page) || 1,
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
aggregate: true,
|
aggregate: true,
|
||||||
}),
|
}, pageContext.user),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
<div
|
<div
|
||||||
v-if="scene.photos.length > 0"
|
v-if="scene.photos.length > 0"
|
||||||
class="album"
|
class="album"
|
||||||
|
:class="{ single: scene.photos.length === 1 }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="photo in scene.photos"
|
v-for="photo in scene.photos"
|
||||||
|
@ -92,7 +93,10 @@
|
||||||
>{{ scene.title }}</h2>
|
>{{ scene.title }}</h2>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="bookmarks">
|
<div
|
||||||
|
v-if="user"
|
||||||
|
class="bookmarks"
|
||||||
|
>
|
||||||
<Icon icon="folder-heart" />
|
<Icon icon="folder-heart" />
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -103,7 +107,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-show="!favorited && user"
|
v-show="!favorited"
|
||||||
icon="heart8"
|
icon="heart8"
|
||||||
class="heart"
|
class="heart"
|
||||||
@click.native.stop="stash"
|
@click.native.stop="stash"
|
||||||
|
@ -179,7 +183,11 @@
|
||||||
<div class="section details">
|
<div class="section details">
|
||||||
<div class="detail">
|
<div class="detail">
|
||||||
<h3 class="heading">Added</h3>
|
<h3 class="heading">Added</h3>
|
||||||
<span class="added-date">{{ formatDate(scene.createdAt, 'yyyy-MM-dd hh:mm') }}</span> <span class="added-batch">batch #{{ scene.createdBatchId }}</span>
|
<span class="added-date">{{ formatDate(scene.createdAt, 'yyyy-MM-dd') }}</span>
|
||||||
|
<span
|
||||||
|
:title="`Batch ${scene.createdBatchId}`"
|
||||||
|
class="added-batch"
|
||||||
|
>#{{ scene.createdBatchId }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -246,28 +254,29 @@ async function unstash() {
|
||||||
.banner-container {
|
.banner-container {
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: .25rem .5rem 0 0;
|
border-radius: .5rem .5rem 0 0;
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
max-height: 21rem;
|
max-height: 21rem;
|
||||||
border-radius: .5rem .5rem 0 0;
|
border-radius: .5rem 0 0 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
backdrop-filter: blur(1rem);
|
backdrop-filter: brightness(150%) blur(1rem);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poster-container {
|
.poster-container {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: .5rem;
|
margin-right: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poster {
|
.poster {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.poster,
|
.poster,
|
||||||
|
@ -282,19 +291,33 @@ async function unstash() {
|
||||||
height: auto;
|
height: auto;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||||
gap: .5rem;
|
gap: .25rem;
|
||||||
|
box-sizing: border-box;
|
||||||
padding: .5rem .5rem .5rem 0;
|
padding: .5rem .5rem .5rem 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
scrollbar-width: 0;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.single .photo {
|
||||||
|
max-height: calc(100% - 1.5rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-container {
|
.photo-container {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.photo-link {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.photo {
|
.photo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -341,7 +364,7 @@ async function unstash() {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1rem 1rem 1.5rem 1rem;
|
padding: 1rem 1rem 0.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@ -447,7 +470,8 @@ async function unstash() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.added-batch {
|
.added-batch {
|
||||||
color: var(--shadow);
|
color: var(--shadow-weak-10);
|
||||||
|
margin-left: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(--small-10) {
|
@media(--small-10) {
|
||||||
|
@ -473,6 +497,14 @@ async function unstash() {
|
||||||
.album {
|
.album {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1rem .5rem 1.5rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
padding: 0 .5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (--small) {
|
@media (--small) {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { fetchScenesById } from '#/src/scenes.js';
|
import { fetchScenesById } from '#/src/scenes.js';
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
const [scene] = await fetchScenesById([Number(pageContext.routeParams.sceneId)], pageContext.user);
|
const [scene] = await fetchScenesById([Number(pageContext.routeParams.sceneId)], {
|
||||||
|
reqUser: pageContext.user,
|
||||||
|
actorStashes: true,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<Stash>
|
||||||
|
<Actors />
|
||||||
|
</Stash>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Stash from '#/components/stashes/stash.vue';
|
||||||
|
import Actors from '#/components/actors/actors.vue';
|
||||||
|
</script>
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||||
|
|
||||||
|
import { fetchStashByUsernameAndSlug } from '#/src/stashes.js';
|
||||||
|
import { fetchActors } from '#/src/actors.js';
|
||||||
|
import { curateActorsQuery } from '#/src/web/actors.js';
|
||||||
|
import { HttpError } from '#/src/errors.js';
|
||||||
|
|
||||||
|
export async function onBeforeRender(pageContext) {
|
||||||
|
try {
|
||||||
|
const stash = await fetchStashByUsernameAndSlug(pageContext.routeParams.username, pageContext.routeParams.stashSlug, pageContext.user);
|
||||||
|
|
||||||
|
const stashActors = await fetchActors(curateActorsQuery({
|
||||||
|
...pageContext.urlQuery,
|
||||||
|
stashId: stash.id,
|
||||||
|
}), {
|
||||||
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
|
limit: Number(pageContext.urlParsed.search.limit) || 120,
|
||||||
|
order: pageContext.urlParsed.search.order?.split('.') || ['stashed', 'desc'],
|
||||||
|
}, pageContext.user);
|
||||||
|
|
||||||
|
const {
|
||||||
|
actors,
|
||||||
|
countries,
|
||||||
|
cupRange,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
} = stashActors;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContext: {
|
||||||
|
title: `${stash.name} by ${stash.user.username}`,
|
||||||
|
pageProps: {
|
||||||
|
stash,
|
||||||
|
actors,
|
||||||
|
countries,
|
||||||
|
cupRange,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
throw render(error.httpCode, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { match } from 'path-to-regexp';
|
||||||
|
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||||
|
|
||||||
|
const path = '/stash/:username/:stashSlug/:domain(actors)/:page?';
|
||||||
|
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||||
|
|
||||||
|
export default (pageContext) => {
|
||||||
|
const matched = urlMatch(pageContext.urlPathname);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
return {
|
||||||
|
routeParams: {
|
||||||
|
username: matched.params.username,
|
||||||
|
stashSlug: matched.params.stashSlug,
|
||||||
|
domain: matched.params.domain,
|
||||||
|
order: 'stashed.desc',
|
||||||
|
page: matched.params.page || '1',
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<Stash>
|
||||||
|
<h2>Movies</h2>
|
||||||
|
</Stash>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Stash from '#/components/stashes/stash.vue';
|
||||||
|
// import Actors from '#/components/actors/actors.vue';
|
||||||
|
</script>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||||
|
|
||||||
|
import { fetchStashByUsernameAndSlug } from '#/src/stashes.js';
|
||||||
|
import { HttpError } from '#/src/errors.js';
|
||||||
|
|
||||||
|
export async function onBeforeRender(pageContext) {
|
||||||
|
try {
|
||||||
|
const stash = await fetchStashByUsernameAndSlug(pageContext.routeParams.username, pageContext.routeParams.stashSlug, pageContext.user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContext: {
|
||||||
|
title: `${stash.name} by ${stash.user.username}`,
|
||||||
|
pageProps: {
|
||||||
|
stash,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
throw render(error.httpCode, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { match } from 'path-to-regexp';
|
import { match } from 'path-to-regexp';
|
||||||
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||||
|
|
||||||
const path = '/stash/:username/:stashSlug/:scope?/:page?';
|
const path = '/stash/:username/:stashSlug/:domain(movies)/:scope?/:page?';
|
||||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||||
|
|
||||||
export default (pageContext) => {
|
export default (pageContext) => {
|
||||||
|
@ -12,6 +12,7 @@ export default (pageContext) => {
|
||||||
routeParams: {
|
routeParams: {
|
||||||
username: matched.params.username,
|
username: matched.params.username,
|
||||||
stashSlug: matched.params.stashSlug,
|
stashSlug: matched.params.stashSlug,
|
||||||
|
domain: matched.params.domain,
|
||||||
scope: matched.params.scope || 'stashed',
|
scope: matched.params.scope || 'stashed',
|
||||||
page: matched.params.page || '1',
|
page: matched.params.page || '1',
|
||||||
path,
|
path,
|
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<Stash>
|
||||||
|
<Scenes />
|
||||||
|
</Stash>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Stash from '#/components/stashes/stash.vue';
|
||||||
|
import Scenes from '#/components/scenes/scenes.vue';
|
||||||
|
</script>
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { match } from 'path-to-regexp';
|
||||||
|
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||||
|
|
||||||
|
const path = '/stash/:username/:stashSlug/:domain?/:scope?/:page?';
|
||||||
|
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||||
|
|
||||||
|
export default (pageContext) => {
|
||||||
|
const matched = urlMatch(pageContext.urlPathname);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
return {
|
||||||
|
routeParams: {
|
||||||
|
username: matched.params.username,
|
||||||
|
stashSlug: matched.params.stashSlug,
|
||||||
|
domain: matched.params.domain || 'scenes',
|
||||||
|
scope: matched.params.scope || 'stashed',
|
||||||
|
page: matched.params.page || '1',
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -1,3 +1,10 @@
|
||||||
export default {
|
export default {
|
||||||
passToClient: ['pageProps', 'urlPathname', 'routeParams', 'urlParsed', 'env', 'user'],
|
passToClient: [
|
||||||
|
'pageProps',
|
||||||
|
'urlPathname',
|
||||||
|
'routeParams',
|
||||||
|
'urlParsed',
|
||||||
|
'env',
|
||||||
|
'user',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,11 +23,23 @@
|
||||||
class="nav"
|
class="nav"
|
||||||
@sidebar="showSidebar = true"
|
@sidebar="showSidebar = true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="feedbackContainer"
|
||||||
|
class="feedback-container"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="feedback"
|
||||||
|
ref="feedbackBubble"
|
||||||
|
class="feedback"
|
||||||
|
:class="{ [feedback.type]: true }"
|
||||||
|
>{{ feedback.message }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, nextTick } from 'vue';
|
||||||
|
|
||||||
import events from '#/src/events.js';
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
|
@ -36,6 +48,9 @@ import Sidebar from '#/components/sidebar/sidebar.vue';
|
||||||
import BottomNavigation from '#/components/footer/navigation.vue';
|
import BottomNavigation from '#/components/footer/navigation.vue';
|
||||||
|
|
||||||
const content = ref(null);
|
const content = ref(null);
|
||||||
|
const feedback = ref(null);
|
||||||
|
const feedbackContainer = ref(null);
|
||||||
|
const feedbackBubble = ref(null);
|
||||||
const showSidebar = ref(false);
|
const showSidebar = ref(false);
|
||||||
|
|
||||||
function blur() {
|
function blur() {
|
||||||
|
@ -44,6 +59,38 @@ function blur() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
events.on('scrollUp', () => { content.value.scrollTop = 0; });
|
events.on('scrollUp', () => { content.value.scrollTop = 0; });
|
||||||
|
|
||||||
|
events.on('feedback', async (event) => {
|
||||||
|
console.log(event);
|
||||||
|
|
||||||
|
feedback.value = event;
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
feedbackBubble.value.animate([
|
||||||
|
{
|
||||||
|
visibility: 'visible',
|
||||||
|
transform: 'scaleY(0)',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'scaleY(1)',
|
||||||
|
opacity: 1,
|
||||||
|
offset: 0.002,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
transform: 'scaleY(1)',
|
||||||
|
opacity: 1,
|
||||||
|
offset: 0.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
], {
|
||||||
|
duration: 3000,
|
||||||
|
easing: 'ease-in-out',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -102,9 +149,47 @@ onMounted(() => {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feedback-container {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1rem;;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
margin: 0 .5rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||||
|
background: var(--grey-dark-40);
|
||||||
|
color: var(--text-light);
|
||||||
|
font-size: .9rem;
|
||||||
|
visibility: hidden;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.remove {
|
||||||
|
background: var(--warn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media(--small-10) {
|
@media(--small-10) {
|
||||||
.nav {
|
.nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feedback-container {
|
||||||
|
bottom: 4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
// `usePageContext` allows us to access `pageContext` in any Vue component.
|
// `usePageContext` allows us to access `pageContext` in any Vue component.
|
||||||
// See https://vike.dev/pageContext-anywhere
|
// See https://vike.dev/pageContext-anywhere
|
||||||
|
|
||||||
import { inject } from 'vue'
|
import { inject } from 'vue';
|
||||||
|
|
||||||
export { usePageContext }
|
const key = Symbol(); // eslint-disable-line symbol-description
|
||||||
export { setPageContext }
|
|
||||||
|
|
||||||
const key = Symbol()
|
export function usePageContext() {
|
||||||
|
const pageContext = inject(key);
|
||||||
function usePageContext() {
|
return pageContext;
|
||||||
const pageContext = inject(key)
|
|
||||||
return pageContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPageContext(app, pageContext) {
|
export function setPageContext(app, pageContext) {
|
||||||
app.provide(key, pageContext)
|
app.provide(key, pageContext);
|
||||||
}
|
}
|
||||||
|
|
244
src/actors.js
244
src/actors.js
|
@ -2,10 +2,11 @@ import config from 'config';
|
||||||
import { differenceInYears } from 'date-fns';
|
import { differenceInYears } from 'date-fns';
|
||||||
import { unit } from 'mathjs';
|
import { unit } from 'mathjs';
|
||||||
|
|
||||||
import knex from './knex.js';
|
import { knexOwner as knex, knexManticore } from './knex.js';
|
||||||
import { searchApi } from './manticore.js';
|
import { utilsApi } from './manticore.js';
|
||||||
import { HttpError } from './errors.js';
|
import { HttpError } from './errors.js';
|
||||||
import { fetchCountriesByAlpha2 } from './countries.js';
|
import { fetchCountriesByAlpha2 } from './countries.js';
|
||||||
|
import { curateStash } from './stashes.js';
|
||||||
|
|
||||||
export function curateActor(actor, context = {}) {
|
export function curateActor(actor, context = {}) {
|
||||||
return {
|
return {
|
||||||
|
@ -58,6 +59,7 @@ export function curateActor(actor, context = {}) {
|
||||||
createdAt: actor.created_at,
|
createdAt: actor.created_at,
|
||||||
updatedAt: actor.updated_at,
|
updatedAt: actor.updated_at,
|
||||||
likes: actor.stashed,
|
likes: actor.stashed,
|
||||||
|
stashes: context.stashes?.map((stash) => curateStash(stash)) || [],
|
||||||
...context.append?.[actor.id],
|
...context.append?.[actor.id],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -73,8 +75,8 @@ export function sortActorsByGender(actors) {
|
||||||
return genderActors;
|
return genderActors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchActorsById(actorIds, options = {}) {
|
export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||||
const [actors] = await Promise.all([
|
const [actors, stashes] = await Promise.all([
|
||||||
knex('actors')
|
knex('actors')
|
||||||
.select(
|
.select(
|
||||||
'actors.*',
|
'actors.*',
|
||||||
|
@ -93,10 +95,19 @@ export async function fetchActorsById(actorIds, options = {}) {
|
||||||
builder.orderBy(...options.order);
|
builder.orderBy(...options.order);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
reqUser
|
||||||
|
? knex('stashes_actors')
|
||||||
|
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
|
||||||
|
.where('stashes.user_id', reqUser.id)
|
||||||
|
.whereIn('stashes_actors.actor_id', actorIds)
|
||||||
|
: [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (options.order) {
|
if (options.order) {
|
||||||
return actors.map((actorEntry) => curateActor(actorEntry, { append: options.append }));
|
return actors.map((actorEntry) => curateActor(actorEntry, {
|
||||||
|
stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
|
||||||
|
append: options.append,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const curatedActors = actorIds.map((actorId) => {
|
const curatedActors = actorIds.map((actorId) => {
|
||||||
|
@ -107,7 +118,10 @@ export async function fetchActorsById(actorIds, options = {}) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return curateActor(actor, { append: options.append });
|
return curateActor(actor, {
|
||||||
|
stashes: stashes.filter((stash) => stash.actor_id === actor.id),
|
||||||
|
append: options.append,
|
||||||
|
});
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
return curatedActors;
|
return curatedActors;
|
||||||
|
@ -126,6 +140,30 @@ 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) {
|
function buildQuery(filters) {
|
||||||
const query = {
|
const query = {
|
||||||
bool: {
|
bool: {
|
||||||
|
@ -230,31 +268,7 @@ function buildQuery(filters) {
|
||||||
return { query, expressions };
|
return { query, expressions };
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortMap = {
|
async function queryManticoreJson(filters, options) {
|
||||||
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
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchActors(filters, rawOptions) {
|
|
||||||
const options = curateOptions(rawOptions);
|
|
||||||
const { query, expressions } = buildQuery(filters);
|
const { query, expressions } = buildQuery(filters);
|
||||||
|
|
||||||
const result = await searchApi.search({
|
const result = await searchApi.search({
|
||||||
|
@ -279,16 +293,176 @@ export async function fetchActors(filters, rawOptions) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const actorIds = result.hits.hits.map((hit) => Number(hit._id));
|
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;
|
||||||
|
|
||||||
|
const sqlQuery = knexManticore.raw(`
|
||||||
|
:query:
|
||||||
|
OPTION
|
||||||
|
max_matches=:maxMatches:,
|
||||||
|
max_query_time=:maxQueryTime:
|
||||||
|
:countriesFacet:;
|
||||||
|
show meta;
|
||||||
|
`, {
|
||||||
|
query: knexManticore(filters.stashId ? 'actors_stashed' : 'actors')
|
||||||
|
.modify((builder) => {
|
||||||
|
if (filters.stashId) {
|
||||||
|
builder.select(knex.raw(`
|
||||||
|
actors.id as id,
|
||||||
|
actors.country as country,
|
||||||
|
actors.scenes as scenes,
|
||||||
|
actors.stashed as stashed,
|
||||||
|
created_at as stashed_at
|
||||||
|
`));
|
||||||
|
// weight() as _score
|
||||||
|
|
||||||
|
builder
|
||||||
|
.innerJoin('actors', 'actors.id', 'actors_stashed.actor_id')
|
||||||
|
.where('stash_id', filters.stashId);
|
||||||
|
} else {
|
||||||
|
// builder.select(knex.raw('*, weight() as _score'));
|
||||||
|
builder.select(knex.raw('*'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.query) {
|
||||||
|
builder.whereRaw('match(\'@name :query:\', actors)', { query: filters.query });
|
||||||
|
}
|
||||||
|
|
||||||
|
['gender', 'country'].forEach((attribute) => {
|
||||||
|
if (filters[attribute]) {
|
||||||
|
builder.where(attribute, filters[attribute]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
['age', 'height', 'weight'].forEach((attribute) => {
|
||||||
|
if (filters[attribute]) {
|
||||||
|
builder
|
||||||
|
.where(attribute, '>=', filters[attribute][0])
|
||||||
|
.where(attribute, '<=', filters[attribute][1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filters.dateOfBirth && filters.dobType === 'dateOfBirth') {
|
||||||
|
builder.where('date_of_birth', Math.floor(filters.dateOfBirth.getTime() / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.dateOfBirth && filters.dobType === 'birthday') {
|
||||||
|
const month = filters.dateOfBirth.getMonth() + 1;
|
||||||
|
const day = filters.dateOfBirth.getDate();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.where('month(date_of_birth)', month)
|
||||||
|
.where('day(date_of_birth)', day);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.cup) {
|
||||||
|
builder.where(`regex(cup, '^[${filters.cup[0]}-${filters.cup[1]}]')`, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof filters.naturalBoobs === 'boolean') {
|
||||||
|
builder.where('natural_boobs', filters.naturalBoobs ? 2 : 1); // manticore boolean does not support null, so 0 = null, 1 = false (enhanced), 2 = true (natural)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.requireAvatar) {
|
||||||
|
builder.where('has_avatar', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.order?.[0] === 'name') {
|
||||||
|
builder.orderBy('actors.slug', options.order[1]);
|
||||||
|
} else if (options.order?.[0] === 'likes') {
|
||||||
|
builder.orderBy([
|
||||||
|
{ column: 'actors.stashed', order: options.order[1] },
|
||||||
|
{ column: 'actors.slug', order: 'asc' },
|
||||||
|
]);
|
||||||
|
} else if (options.order?.[0] === 'scenes') {
|
||||||
|
builder.orderBy([
|
||||||
|
{ column: 'actors.scenes', order: options.order[1] },
|
||||||
|
{ column: 'actors.slug', order: 'asc' },
|
||||||
|
]);
|
||||||
|
} else if (options.order?.[0] === 'stashed' && filters.stashId) {
|
||||||
|
builder.orderBy([
|
||||||
|
{ column: 'stashed_at', order: options.order[1] },
|
||||||
|
{ column: 'actors.slug', order: 'asc' },
|
||||||
|
]);
|
||||||
|
} else if (options.order) {
|
||||||
|
builder.orderBy([
|
||||||
|
{ column: `actors.${options.order[0]}`, order: options.order[1] },
|
||||||
|
{ column: 'actors.slug', order: 'asc' },
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
builder.orderBy('actors.slug', 'asc');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.limit(options.limit)
|
||||||
|
.offset((options.page - 1) * options.limit)
|
||||||
|
.toString(),
|
||||||
|
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
|
||||||
|
countriesFacet: options.aggregateActors ? knex.raw('facet actors.country order by count(*) desc limit 300', [aggSize]) : null,
|
||||||
|
maxMatches: config.database.manticore.maxMatches,
|
||||||
|
maxQueryTime: config.database.manticore.maxQueryTime,
|
||||||
|
}).toString();
|
||||||
|
|
||||||
|
// manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
|
||||||
|
const curatedSqlQuery = filters.stashId
|
||||||
|
? sqlQuery
|
||||||
|
: sqlQuery.replace(/actors\./g, '');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log(curatedSqlQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await utilsApi.sql(curatedSqlQuery);
|
||||||
|
|
||||||
|
// console.log(results[0]);
|
||||||
|
|
||||||
|
const countries = results
|
||||||
|
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.country']) && result.columns[1]['count(*)'])
|
||||||
|
?.data.map((row) => ({ key: row.actor_ids || row['scenes.country'], doc_count: row['count(*)'] }))
|
||||||
|
|| [];
|
||||||
|
|
||||||
|
const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found').Value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
actors: results[0].data,
|
||||||
|
total,
|
||||||
|
aggregations: {
|
||||||
|
countries,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchActors(filters, rawOptions, reqUser) {
|
||||||
|
const options = curateOptions(rawOptions);
|
||||||
|
|
||||||
|
console.log('filters', filters);
|
||||||
|
console.log('options', options);
|
||||||
|
|
||||||
|
const result = await queryManticoreSql(filters, options, reqUser);
|
||||||
|
|
||||||
|
const actorIds = result.actors.map((actor) => Number(actor.id));
|
||||||
const [actors, countries] = await Promise.all([
|
const [actors, countries] = await Promise.all([
|
||||||
fetchActorsById(actorIds),
|
fetchActorsById(actorIds, {}, reqUser),
|
||||||
fetchCountriesByAlpha2(result.aggregations.countries.buckets.map((bucket) => bucket.key)),
|
fetchCountriesByAlpha2(result.aggregations.countries.map((bucket) => bucket.key)),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actors,
|
actors,
|
||||||
countries,
|
countries,
|
||||||
total: result.hits.total,
|
total: result.total,
|
||||||
limit: options.limit,
|
limit: options.limit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import config from 'config';
|
||||||
import util from 'util'; /* eslint-disable-line no-unused-vars */
|
import util from 'util'; /* eslint-disable-line no-unused-vars */
|
||||||
|
|
||||||
import { knexOwner as knex, knexManticore } from './knex.js';
|
import { knexOwner as knex, knexManticore } from './knex.js';
|
||||||
import { searchApi, utilsApi } from './manticore.js';
|
import { utilsApi } from './manticore.js';
|
||||||
import { HttpError } from './errors.js';
|
import { HttpError } from './errors.js';
|
||||||
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
|
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
|
||||||
import { fetchTagsById } from './tags.js';
|
import { fetchTagsById } from './tags.js';
|
||||||
|
@ -55,7 +55,10 @@ function curateScene(rawScene, assets) {
|
||||||
type: assets.channel.network_type,
|
type: assets.channel.network_type,
|
||||||
hasLogo: assets.channel.has_logo,
|
hasLogo: assets.channel.has_logo,
|
||||||
} : null,
|
} : null,
|
||||||
actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, { sceneDate: rawScene.effective_date }))),
|
actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, {
|
||||||
|
sceneDate: rawScene.effective_date,
|
||||||
|
stashes: assets.actorStashes,
|
||||||
|
}))),
|
||||||
directors: assets.directors.map((director) => ({
|
directors: assets.directors.map((director) => ({
|
||||||
id: director.id,
|
id: director.id,
|
||||||
slug: director.slug,
|
slug: director.slug,
|
||||||
|
@ -74,7 +77,7 @@ function curateScene(rawScene, assets) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchScenesById(sceneIds, reqUser) {
|
export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||||
const [scenes, channels, actors, directors, tags, posters, photos, stashes] = await Promise.all([
|
const [scenes, channels, actors, directors, tags, posters, photos, stashes] = await Promise.all([
|
||||||
knex('releases').whereIn('releases.id', sceneIds),
|
knex('releases').whereIn('releases.id', sceneIds),
|
||||||
knex('releases')
|
knex('releases')
|
||||||
|
@ -125,6 +128,13 @@ export async function fetchScenesById(sceneIds, reqUser) {
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const actorStashes = reqUser && context.actorStashes
|
||||||
|
? await knex('stashes_actors')
|
||||||
|
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
|
||||||
|
.where('stashes.user_id', reqUser.id)
|
||||||
|
.whereIn('stashes_actors.actor_id', actors.map((actor) => actor.id))
|
||||||
|
: [];
|
||||||
|
|
||||||
return sceneIds.map((sceneId) => {
|
return sceneIds.map((sceneId) => {
|
||||||
const scene = scenes.find((sceneEntry) => sceneEntry.id === sceneId);
|
const scene = scenes.find((sceneEntry) => sceneEntry.id === sceneId);
|
||||||
|
|
||||||
|
@ -139,6 +149,7 @@ export async function fetchScenesById(sceneIds, reqUser) {
|
||||||
const scenePoster = posters.find((poster) => poster.release_id === sceneId);
|
const scenePoster = posters.find((poster) => poster.release_id === sceneId);
|
||||||
const scenePhotos = photos.filter((photo) => photo.release_id === sceneId);
|
const scenePhotos = photos.filter((photo) => photo.release_id === sceneId);
|
||||||
const sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
|
const sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
|
||||||
|
const sceneActorStashes = sceneActors.map((actor) => actorStashes.find((stash) => stash.actor_id === actor.id)).filter(Boolean);
|
||||||
|
|
||||||
return curateScene(scene, {
|
return curateScene(scene, {
|
||||||
channel: sceneChannel,
|
channel: sceneChannel,
|
||||||
|
@ -148,6 +159,7 @@ export async function fetchScenesById(sceneIds, reqUser) {
|
||||||
poster: scenePoster,
|
poster: scenePoster,
|
||||||
photos: scenePhotos,
|
photos: scenePhotos,
|
||||||
stashes: sceneStashes,
|
stashes: sceneStashes,
|
||||||
|
actorStashes: sceneActorStashes,
|
||||||
});
|
});
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
@ -171,6 +183,7 @@ function curateOptions(options) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
function buildQuery(filters = {}, options) {
|
function buildQuery(filters = {}, options) {
|
||||||
const query = {
|
const query = {
|
||||||
bool: {
|
bool: {
|
||||||
|
@ -215,23 +228,6 @@ function buildQuery(filters = {}, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.query) {
|
if (filters.query) {
|
||||||
/*
|
|
||||||
query.bool.must.push({
|
|
||||||
bool: {
|
|
||||||
should: [
|
|
||||||
{ match: { title_filtered: filters.query } },
|
|
||||||
{ match: { actors: filters.query } },
|
|
||||||
{ match: { tags: filters.query } },
|
|
||||||
{ match: { channel_name: filters.query } },
|
|
||||||
{ match: { network_name: filters.query } },
|
|
||||||
{ match: { channel_slug: filters.query } },
|
|
||||||
{ match: { network_slug: filters.query } },
|
|
||||||
{ match: { meta: filters.query } }, // date
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
query.bool.must.push({ match: { '!title': filters.query } }); // title_filtered is matched instead of title
|
query.bool.must.push({ match: { '!title': filters.query } }); // title_filtered is matched instead of title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,16 +258,6 @@ function buildQuery(filters = {}, options) {
|
||||||
query.bool.must.push({ equals: { stash_id: filters.stashId } });
|
query.bool.must.push({ equals: { stash_id: filters.stashId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tag filter
|
|
||||||
must_not: [
|
|
||||||
{
|
|
||||||
in: {
|
|
||||||
'any(tag_ids)': [101, 180, 32],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
*/
|
|
||||||
|
|
||||||
return { query, sort };
|
return { query, sort };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,14 +297,6 @@ function buildAggregates(options) {
|
||||||
return aggregates;
|
return aggregates;
|
||||||
}
|
}
|
||||||
|
|
||||||
function countAggregations(buckets) {
|
|
||||||
if (!buckets) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function queryManticoreJson(filters, options, _reqUser) {
|
async function queryManticoreJson(filters, options, _reqUser) {
|
||||||
const { query, sort } = buildQuery(filters, options);
|
const { query, sort } = buildQuery(filters, options);
|
||||||
|
|
||||||
|
@ -357,13 +335,15 @@ async function queryManticoreJson(filters, options, _reqUser) {
|
||||||
aggregations: result.aggregations && Object.fromEntries(Object.entries(result.aggregations).map(([key, { buckets }]) => [key, buckets])),
|
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;
|
||||||
|
|
||||||
const sqlQuery = knexManticore.raw(`
|
const sqlQuery = knexManticore.raw(`
|
||||||
:query:
|
:query:
|
||||||
OPTION field_weights=(
|
OPTION
|
||||||
|
field_weights=(
|
||||||
title_filtered=7,
|
title_filtered=7,
|
||||||
actors=10,
|
actors=10,
|
||||||
tags=9,
|
tags=9,
|
||||||
|
@ -391,6 +371,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||||
scenes.channel_id as channel_id,
|
scenes.channel_id as channel_id,
|
||||||
scenes.network_id as network_id,
|
scenes.network_id as network_id,
|
||||||
scenes.effective_date as effective_date,
|
scenes.effective_date as effective_date,
|
||||||
|
scenes.stashed as stashed,
|
||||||
scenes.created_at,
|
scenes.created_at,
|
||||||
created_at as stashed_at,
|
created_at as stashed_at,
|
||||||
weight() as _score
|
weight() as _score
|
||||||
|
@ -481,7 +462,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||||
|
|
||||||
const results = await utilsApi.sql(curatedSqlQuery);
|
const results = await utilsApi.sql(curatedSqlQuery);
|
||||||
|
|
||||||
console.log(results[0]);
|
// console.log(results[0]);
|
||||||
|
|
||||||
const actorIds = results
|
const actorIds = results
|
||||||
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.actor_ids']) && result.columns[1]['count(*)'])
|
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.actor_ids']) && result.columns[1]['count(*)'])
|
||||||
|
@ -511,6 +492,14 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function countAggregations(buckets) {
|
||||||
|
if (!buckets) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchScenes(filters, rawOptions, reqUser) {
|
export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||||
const options = curateOptions(rawOptions);
|
const options = curateOptions(rawOptions);
|
||||||
|
|
||||||
|
@ -527,10 +516,6 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||||
const result = await queryManticoreSql(filters, options, reqUser);
|
const result = await queryManticoreSql(filters, options, reqUser);
|
||||||
console.timeEnd('manticore sql');
|
console.timeEnd('manticore sql');
|
||||||
|
|
||||||
console.time('manticore json');
|
|
||||||
await queryManticoreJson(filters, options, reqUser);
|
|
||||||
console.timeEnd('manticore json');
|
|
||||||
|
|
||||||
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds);
|
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds);
|
||||||
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
|
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
|
||||||
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
||||||
|
@ -547,7 +532,7 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||||
|
|
||||||
console.time('fetch full');
|
console.time('fetch full');
|
||||||
const sceneIds = result.scenes.map((scene) => Number(scene.id));
|
const sceneIds = result.scenes.map((scene) => Number(scene.id));
|
||||||
const scenes = await fetchScenesById(sceneIds, reqUser);
|
const scenes = await fetchScenesById(sceneIds, { reqUser });
|
||||||
console.timeEnd('fetch full');
|
console.timeEnd('fetch full');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -170,12 +170,26 @@ export async function refreshActorsView() {
|
||||||
export async function stashActor(actorId, stashId, sessionUser) {
|
export async function stashActor(actorId, stashId, sessionUser) {
|
||||||
const stash = await fetchStashById(stashId, sessionUser);
|
const stash = await fetchStashById(stashId, sessionUser);
|
||||||
|
|
||||||
await knex('stashes_actors')
|
const [stashed] = await knex('stashes_actors')
|
||||||
.insert({
|
.insert({
|
||||||
stash_id: stash.id,
|
stash_id: stash.id,
|
||||||
actor_id: actorId,
|
actor_id: actorId,
|
||||||
|
})
|
||||||
|
.returning(['id', 'created_at']);
|
||||||
|
|
||||||
|
await indexApi.replace({
|
||||||
|
index: 'actors_stashed',
|
||||||
|
id: stashed.id,
|
||||||
|
doc: {
|
||||||
|
actor_id: actorId,
|
||||||
|
user_id: sessionUser.id,
|
||||||
|
stash_id: stashId,
|
||||||
|
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed actor ${actorId} to stash ${stash.id} (${stash.name})`);
|
||||||
|
|
||||||
refreshActorsView();
|
refreshActorsView();
|
||||||
|
|
||||||
return fetchStashes('actor', actorId, sessionUser);
|
return fetchStashes('actor', actorId, sessionUser);
|
||||||
|
@ -192,6 +206,25 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
||||||
.where('stashes.user_id', sessionUser.id))
|
.where('stashes.user_id', sessionUser.id))
|
||||||
.delete();
|
.delete();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await indexApi.callDelete({
|
||||||
|
index: 'actors_stashed',
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
must: [
|
||||||
|
{ equals: { actor_id: actorId } },
|
||||||
|
{ equals: { stash_id: stashId } },
|
||||||
|
{ equals: { user_id: sessionUser.id } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId}`);
|
||||||
|
|
||||||
refreshActorsView();
|
refreshActorsView();
|
||||||
|
|
||||||
return fetchStashes('actor', actorId, sessionUser);
|
return fetchStashes('actor', actorId, sessionUser);
|
||||||
|
@ -200,12 +233,27 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
||||||
export async function stashScene(sceneId, stashId, sessionUser) {
|
export async function stashScene(sceneId, stashId, sessionUser) {
|
||||||
const stash = await fetchStashById(stashId, sessionUser);
|
const stash = await fetchStashById(stashId, sessionUser);
|
||||||
|
|
||||||
await knex('stashes_scenes')
|
const [stashed] = await knex('stashes_scenes')
|
||||||
.insert({
|
.insert({
|
||||||
stash_id: stash.id,
|
stash_id: stash.id,
|
||||||
scene_id: sceneId,
|
scene_id: sceneId,
|
||||||
|
})
|
||||||
|
.returning(['id', 'created_at']);
|
||||||
|
|
||||||
|
await indexApi.replace({
|
||||||
|
index: 'scenes_stashed',
|
||||||
|
id: stashed.id,
|
||||||
|
doc: {
|
||||||
|
// ...doc.replace.doc,
|
||||||
|
scene_id: sceneId,
|
||||||
|
user_id: sessionUser.id,
|
||||||
|
stash_id: stashId,
|
||||||
|
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed scene ${sceneId} to stash ${stash.id} (${stash.name})`);
|
||||||
|
|
||||||
return fetchStashes('scene', sceneId, sessionUser);
|
return fetchStashes('scene', sceneId, sessionUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +273,7 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
|
||||||
query: {
|
query: {
|
||||||
bool: {
|
bool: {
|
||||||
must: [
|
must: [
|
||||||
{ equals: { id: sceneId } },
|
{ equals: { scene_id: sceneId } },
|
||||||
{ equals: { stash_id: stashId } },
|
{ equals: { stash_id: stashId } },
|
||||||
{ equals: { user_id: sessionUser.id } },
|
{ equals: { user_id: sessionUser.id } },
|
||||||
],
|
],
|
||||||
|
@ -233,6 +281,8 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stashId}`);
|
||||||
|
|
||||||
return fetchStashes('scene', sceneId, sessionUser);
|
return fetchStashes('scene', sceneId, sessionUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
|
||||||
import { indexApi, utilsApi } from '../manticore.js';
|
import { indexApi } from '../manticore.js';
|
||||||
|
|
||||||
import { knexOwner as knex } from '../knex.js';
|
import { knexOwner as knex } from '../knex.js';
|
||||||
import slugify from '../utils/slugify.js';
|
import slugify from '../utils/slugify.js';
|
||||||
|
@ -105,11 +105,11 @@ async function updateStashed(docs) {
|
||||||
|
|
||||||
const stashDoc = sceneStashes.map((stash) => ({
|
const stashDoc = sceneStashes.map((stash) => ({
|
||||||
replace: {
|
replace: {
|
||||||
index: 'movies_liked',
|
index: 'scenes_stashed',
|
||||||
id: stash.stashed_id,
|
id: stash.stashed_id,
|
||||||
doc: {
|
doc: {
|
||||||
// ...doc.replace.doc,
|
// ...doc.replace.doc,
|
||||||
movie_id: doc.replace.id,
|
scene_id: doc.replace.id,
|
||||||
user_id: stash.user_id,
|
user_id: stash.user_id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -127,35 +127,6 @@ async function updateStashed(docs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await utilsApi.sql('drop table if exists movies');
|
|
||||||
await utilsApi.sql('drop table if exists movies_liked');
|
|
||||||
|
|
||||||
await utilsApi.sql(`create table movies (
|
|
||||||
id int,
|
|
||||||
title text,
|
|
||||||
title_filtered text,
|
|
||||||
channel_id int,
|
|
||||||
channel_name text,
|
|
||||||
channel_slug text,
|
|
||||||
network_id int,
|
|
||||||
network_name text,
|
|
||||||
network_slug text,
|
|
||||||
actor_ids multi,
|
|
||||||
actors text,
|
|
||||||
tag_ids multi,
|
|
||||||
tags text,
|
|
||||||
meta text,
|
|
||||||
date timestamp,
|
|
||||||
created_at timestamp,
|
|
||||||
effective_date timestamp,
|
|
||||||
liked int
|
|
||||||
)`);
|
|
||||||
|
|
||||||
await utilsApi.sql(`create table movies_liked (
|
|
||||||
movie_id int,
|
|
||||||
user_id int
|
|
||||||
)`);
|
|
||||||
|
|
||||||
const scenes = await fetchScenes();
|
const scenes = await fetchScenes();
|
||||||
|
|
||||||
const docs = scenes.map((scene) => {
|
const docs = scenes.map((scene) => {
|
||||||
|
@ -165,7 +136,7 @@ async function init() {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
replace: {
|
replace: {
|
||||||
index: 'movies',
|
index: 'scenes',
|
||||||
id: scene.id,
|
id: scene.id,
|
||||||
doc: {
|
doc: {
|
||||||
title: scene.title || undefined,
|
title: scene.title || undefined,
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { indexApi } from '../manticore.js';
|
||||||
|
import { knexOwner as knex } from '../knex.js';
|
||||||
|
import chunk from '../utils/chunk.js';
|
||||||
|
|
||||||
|
async function syncActorStashes() {
|
||||||
|
const stashes = await knex('stashes_actors')
|
||||||
|
.select(
|
||||||
|
'stashes_actors.id as stashed_id',
|
||||||
|
'stashes_actors.actor_id',
|
||||||
|
'stashes.id as stash_id',
|
||||||
|
'stashes.user_id as user_id',
|
||||||
|
'stashes_actors.created_at as created_at',
|
||||||
|
)
|
||||||
|
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id');
|
||||||
|
|
||||||
|
if (stashes.length > 0) {
|
||||||
|
console.log(stashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
await chunk(stashes, 1000).reduce(async (chain, stashChunk, index) => {
|
||||||
|
await chain;
|
||||||
|
|
||||||
|
const stashDocs = stashChunk.map((stash) => ({
|
||||||
|
replace: {
|
||||||
|
index: 'actors_stashed',
|
||||||
|
id: stash.stashed_id,
|
||||||
|
doc: {
|
||||||
|
actor_id: stash.actor_id,
|
||||||
|
stash_id: stash.stash_id,
|
||||||
|
user_id: stash.user_id,
|
||||||
|
created_at: Math.round(stash.created_at.getTime() / 1000),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(stashDocs);
|
||||||
|
|
||||||
|
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||||
|
|
||||||
|
console.log(`Synced ${index * 1000 + stashChunk.length}/${stashes.length} actor stashes`);
|
||||||
|
}, Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await syncActorStashes();
|
||||||
|
console.log('Done!');
|
||||||
|
|
||||||
|
knex.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
|
@ -13,6 +13,7 @@ export function curateActorsQuery(query) {
|
||||||
height: query.height?.split(',').map((height) => Number(height)),
|
height: query.height?.split(',').map((height) => Number(height)),
|
||||||
weight: query.weight?.split(',').map((weight) => Number(weight)),
|
weight: query.weight?.split(',').map((weight) => Number(weight)),
|
||||||
requireAvatar: query.avatar,
|
requireAvatar: query.avatar,
|
||||||
|
stashId: Number(query.stashId) || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ export async function fetchActorsApi(req, res) {
|
||||||
page: Number(req.query.page) || 1,
|
page: Number(req.query.page) || 1,
|
||||||
limit: Number(req.query.limit) || 120,
|
limit: Number(req.query.limit) || 120,
|
||||||
order: req.query.order?.split('.') || ['likes', 'desc'],
|
order: req.query.order?.split('.') || ['likes', 'desc'],
|
||||||
});
|
}, req.user);
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
actors,
|
actors,
|
||||||
|
|
|
@ -11,7 +11,7 @@ export async function curateScenesQuery(query) {
|
||||||
actorIds: [query.actorId, ...(query.actors?.split(',') || []).map((identifier) => parseActorIdentifier(identifier)?.id)].filter(Boolean),
|
actorIds: [query.actorId, ...(query.actors?.split(',') || []).map((identifier) => parseActorIdentifier(identifier)?.id)].filter(Boolean),
|
||||||
tagIds: await getIdsBySlug([query.tagSlug, ...(query.tags?.split(',') || [])], 'tags'),
|
tagIds: await getIdsBySlug([query.tagSlug, ...(query.tags?.split(',') || [])], 'tags'),
|
||||||
entityId: query.e ? await getIdsBySlug([query.e], 'entities').then(([id]) => id) : query.entityId,
|
entityId: query.e ? await getIdsBySlug([query.e], 'entities').then(([id]) => id) : query.entityId,
|
||||||
stashId: Number(query.stashId),
|
stashId: Number(query.stashId) || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,37 +17,37 @@ export async function createStashApi(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateStashApi(req, res) {
|
export async function updateStashApi(req, res) {
|
||||||
const stash = await updateStash(req.params.stashId, req.body, req.session.user);
|
const stash = await updateStash(Number(req.params.stashId), req.body, req.session.user);
|
||||||
|
|
||||||
res.send(stash);
|
res.send(stash);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeStashApi(req, res) {
|
export async function removeStashApi(req, res) {
|
||||||
await removeStash(req.params.stashId, req.session.user);
|
await removeStash(Number(req.params.stashId), req.session.user);
|
||||||
|
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stashActorApi(req, res) {
|
export async function stashActorApi(req, res) {
|
||||||
const stashes = await stashActor(req.body.actorId, req.params.stashId, req.user);
|
const stashes = await stashActor(req.body.actorId, Number(req.params.stashId), req.user);
|
||||||
|
|
||||||
res.send(stashes);
|
res.send(stashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stashSceneApi(req, res) {
|
export async function stashSceneApi(req, res) {
|
||||||
const stashes = await stashScene(req.body.sceneId, req.params.stashId, req.user);
|
const stashes = await stashScene(req.body.sceneId, Number(req.params.stashId), req.user);
|
||||||
|
|
||||||
res.send(stashes);
|
res.send(stashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stashMovieApi(req, res) {
|
export async function stashMovieApi(req, res) {
|
||||||
const stashes = await stashMovie(req.body.movieId, req.params.stashId, req.user);
|
const stashes = await stashMovie(req.body.movieId, Number(req.params.stashId), req.user);
|
||||||
|
|
||||||
res.send(stashes);
|
res.send(stashes);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unstashActorApi(req, res) {
|
export async function unstashActorApi(req, res) {
|
||||||
const stashes = await unstashActor(req.params.actorId, req.params.stashId, req.user);
|
const stashes = await unstashActor(Number(req.params.actorId), Number(req.params.stashId), req.user);
|
||||||
|
|
||||||
res.send(stashes);
|
res.send(stashes);
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ export async function unstashSceneApi(req, res) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unstashMovieApi(req, res) {
|
export async function unstashMovieApi(req, res) {
|
||||||
const stashes = await unstashMovie(req.params.movieId, req.params.stashId, req.user);
|
const stashes = await unstashMovie(Number(req.params.movieId), Number(req.params.stashId), req.user);
|
||||||
|
|
||||||
res.send(stashes);
|
res.send(stashes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export default function ellipsis(text, limit = 50, ellipse = '...') {
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.length > limit) {
|
||||||
|
return `${text.slice(0, limit - ellipse.length)}${ellipse}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
Loading…
Reference in New Issue