Added actor stash.
This commit is contained in:
parent
9b50b53df6
commit
a8aab600c7
|
@ -192,3 +192,9 @@
|
|||
.v-popper--theme-dropdown .v-popper__arrow-outer {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.resize-observer {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -1,30 +1,297 @@
|
|||
<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">
|
||||
<li
|
||||
v-for="actor in actors"
|
||||
:key="`actor-${actor.id}`"
|
||||
class="actor"
|
||||
>
|
||||
<ActorTile :actor="actor" />
|
||||
<ActorTile
|
||||
:actor="actor"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Pagination
|
||||
:page="currentPage"
|
||||
:total="total"
|
||||
:redirect="false"
|
||||
@navigation="paginate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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({
|
||||
actors: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
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 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>
|
||||
|
||||
<style scoped>
|
||||
.actors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 10rem);
|
||||
gap: .5rem;
|
||||
<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>
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
<template>
|
||||
<div class="actor">
|
||||
<div
|
||||
class="tile"
|
||||
:class="{
|
||||
unstashed: !favorited && pageStash && user && pageStash.id === user.primaryStash?.id
|
||||
}"
|
||||
>
|
||||
<span class="name">{{ actor.name }}</span>
|
||||
|
||||
<div class="avatar-container">
|
||||
<Link
|
||||
:href="`/actor/${actor.id}/${actor.slug}`"
|
||||
class="avatar-link no-link"
|
||||
|
@ -15,6 +21,21 @@
|
|||
>
|
||||
</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">
|
||||
<span class="birth">
|
||||
<Gender :gender="actor.gender" />
|
||||
|
@ -48,20 +69,72 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
import { post, del } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
|
||||
import Gender from './gender.vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.actor {
|
||||
.tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
@ -79,6 +152,10 @@ defineProps({
|
|||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&.unstashed {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
|
@ -92,9 +169,14 @@ defineProps({
|
|||
user-select: all;
|
||||
}
|
||||
|
||||
.avatar-link {
|
||||
display: block;
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.avatar-link {
|
||||
height: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
@ -107,6 +189,26 @@ defineProps({
|
|||
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 {
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
|
|
|
@ -103,7 +103,7 @@
|
|||
:href="`/user/${user.username}`"
|
||||
class="menu-button nolink"
|
||||
>
|
||||
<Icon icon="vcard" />
|
||||
<Icon icon="user7" />
|
||||
View profile
|
||||
</a>
|
||||
</li>
|
||||
|
@ -332,7 +332,7 @@ async function logout() {
|
|||
.menu-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem .5rem .5rem .75rem;
|
||||
padding: .75rem .5rem .75rem .75rem;
|
||||
font-size: 1.1rem;
|
||||
|
||||
.icon {
|
||||
|
@ -342,8 +342,16 @@ async function logout() {
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-weak-30);
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.logout) .icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
|
||||
&.logout {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
<option value="latest">Latest</option>
|
||||
<option value="upcoming">Upcoming</option>
|
||||
<option value="new">New</option>
|
||||
<option value="likes">Likes</option>
|
||||
<option value="likes">Popular</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -110,6 +110,8 @@ import { ref, inject } from 'vue';
|
|||
import { format } from 'date-fns';
|
||||
|
||||
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';
|
||||
|
||||
|
@ -125,23 +127,47 @@ const user = pageContext.user;
|
|||
const pageStash = pageContext.pageProps.stash;
|
||||
|
||||
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.primary));
|
||||
const fbCutoff = 20;
|
||||
|
||||
async function stash() {
|
||||
try {
|
||||
favorited.value = true;
|
||||
|
||||
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) {
|
||||
favorited.value = false;
|
||||
|
||||
events.emit('feedback', {
|
||||
type: 'error',
|
||||
message: `Failed to stash "${ellipsis(props.scene.title, fbCutoff)}" to ${user.primaryStash.name}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function unstash() {
|
||||
try {
|
||||
favorited.value = false;
|
||||
|
||||
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) {
|
||||
console.error(error);
|
||||
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>
|
||||
|
|
|
@ -26,8 +26,28 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="scenes-container">
|
||||
<Scenes />
|
||||
<div class="content">
|
||||
<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>
|
||||
</template>
|
||||
|
@ -35,10 +55,15 @@
|
|||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { routeParams } = pageContext;
|
||||
|
||||
const domain = routeParams.domain;
|
||||
const stash = pageContext.pageProps.stash;
|
||||
|
||||
function getPath(targetDomain) {
|
||||
return `/stash/${stash.user.username}/${stash.slug}/${targetDomain}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -91,6 +116,31 @@ const stash = pageContext.pageProps.stash;
|
|||
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) {
|
||||
.title {
|
||||
font-size: 1rem;
|
|
@ -73,6 +73,9 @@ module.exports = {
|
|||
usernameLength: [2, 24],
|
||||
usernamePattern: /^[a-zA-Z0-9_-]+$/,
|
||||
},
|
||||
stashes: {
|
||||
viewRefreshCooldown: 60, // minutes
|
||||
},
|
||||
exclude: {
|
||||
channels: [
|
||||
// 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>
|
||||
<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 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>
|
||||
<Actors />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from '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();
|
||||
}
|
||||
}
|
||||
import Actors from '#/components/actors/actors.vue';
|
||||
</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,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 120,
|
||||
order: pageContext.urlParsed.search.order?.split('.') || ['likes', 'desc'],
|
||||
});
|
||||
}, pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
|
|
|
@ -25,20 +25,19 @@
|
|||
</span>
|
||||
</li>
|
||||
|
||||
<!--
|
||||
<Social
|
||||
v-if="actor.social && actor.social.length > 0"
|
||||
:actor="actor"
|
||||
class="header-social"
|
||||
<Icon
|
||||
v-show="favorited"
|
||||
icon="heart7"
|
||||
class="heart favorited"
|
||||
@click.native.stop="unstash"
|
||||
/>
|
||||
|
||||
<StashButton
|
||||
:stashed-by="stashedBy"
|
||||
class="actor-stash light"
|
||||
@stash="(stash) => stashActor(stash)"
|
||||
@unstash="(stash) => unstashActor(stash)"
|
||||
<Icon
|
||||
v-show="!favorited && user"
|
||||
icon="heart8"
|
||||
class="heart"
|
||||
@click.native.stop="stash"
|
||||
/>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
@ -50,15 +49,61 @@
|
|||
</template>
|
||||
|
||||
<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 Gender from '#/components/actors/gender.vue';
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps } = pageContext;
|
||||
const { pageProps, user } = pageContext;
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
@ -73,6 +118,7 @@ const { actor } = pageProps;
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
color: var(--highlight-strong-30);
|
||||
background: var(--grey-dark-40);
|
||||
}
|
||||
|
@ -103,4 +149,24 @@ const { actor } = pageProps;
|
|||
flex-grow: 1;
|
||||
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>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { curateScenesQuery } from '#/src/web/scenes.js';
|
|||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [[actor], actorScenes] = await Promise.all([
|
||||
fetchActorsById([Number(pageContext.routeParams.actorId)]),
|
||||
fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user),
|
||||
fetchScenes(await curateScenesQuery({
|
||||
...pageContext.urlQuery,
|
||||
scope: pageContext.routeParams.scope || 'latest',
|
||||
|
@ -13,7 +13,7 @@ export async function onBeforeRender(pageContext) {
|
|||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
}),
|
||||
}, pageContext.user),
|
||||
]);
|
||||
|
||||
const {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<div
|
||||
v-if="scene.photos.length > 0"
|
||||
class="album"
|
||||
:class="{ single: scene.photos.length === 1 }"
|
||||
>
|
||||
<div
|
||||
v-for="photo in scene.photos"
|
||||
|
@ -92,7 +93,10 @@
|
|||
>{{ scene.title }}</h2>
|
||||
|
||||
<div class="actions">
|
||||
<div class="bookmarks">
|
||||
<div
|
||||
v-if="user"
|
||||
class="bookmarks"
|
||||
>
|
||||
<Icon icon="folder-heart" />
|
||||
|
||||
<Icon
|
||||
|
@ -103,7 +107,7 @@
|
|||
/>
|
||||
|
||||
<Icon
|
||||
v-show="!favorited && user"
|
||||
v-show="!favorited"
|
||||
icon="heart8"
|
||||
class="heart"
|
||||
@click.native.stop="stash"
|
||||
|
@ -179,7 +183,11 @@
|
|||
<div class="section details">
|
||||
<div class="detail">
|
||||
<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
|
||||
|
@ -246,28 +254,29 @@ async function unstash() {
|
|||
.banner-container {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: .25rem .5rem 0 0;
|
||||
border-radius: .5rem .5rem 0 0;
|
||||
margin-top: .5rem;
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.banner {
|
||||
max-height: 21rem;
|
||||
border-radius: .5rem .5rem 0 0;
|
||||
border-radius: .5rem 0 0 0;
|
||||
display: flex;
|
||||
font-size: 0;
|
||||
backdrop-filter: blur(1rem);
|
||||
backdrop-filter: brightness(150%) blur(1rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.poster-container {
|
||||
flex-shrink: 0;
|
||||
padding: .5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.poster {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: .25rem;
|
||||
border-radius: .25rem 0 0 0;
|
||||
}
|
||||
|
||||
.poster,
|
||||
|
@ -282,19 +291,33 @@ async function unstash() {
|
|||
height: auto;
|
||||
flex-grow: 1;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
gap: .5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||
gap: .25rem;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem .5rem .5rem 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: 0;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.single .photo {
|
||||
max-height: calc(100% - 1.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
.photo-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.photo-link {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -341,7 +364,7 @@ async function unstash() {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1rem 1.5rem 1rem;
|
||||
padding: 1rem 1rem 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@ -447,7 +470,8 @@ async function unstash() {
|
|||
}
|
||||
|
||||
.added-batch {
|
||||
color: var(--shadow);
|
||||
color: var(--shadow-weak-10);
|
||||
margin-left: .25rem;
|
||||
}
|
||||
|
||||
@media(--small-10) {
|
||||
|
@ -473,6 +497,14 @@ async function unstash() {
|
|||
.album {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 1rem .5rem 1.5rem .5rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--small) {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { fetchScenesById } from '#/src/scenes.js';
|
||||
|
||||
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 {
|
||||
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 { 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 });
|
||||
|
||||
export default (pageContext) => {
|
||||
|
@ -12,6 +12,7 @@ export default (pageContext) => {
|
|||
routeParams: {
|
||||
username: matched.params.username,
|
||||
stashSlug: matched.params.stashSlug,
|
||||
domain: matched.params.domain,
|
||||
scope: matched.params.scope || 'stashed',
|
||||
page: matched.params.page || '1',
|
||||
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 {
|
||||
passToClient: ['pageProps', 'urlPathname', 'routeParams', 'urlParsed', 'env', 'user'],
|
||||
passToClient: [
|
||||
'pageProps',
|
||||
'urlPathname',
|
||||
'routeParams',
|
||||
'urlParsed',
|
||||
'env',
|
||||
'user',
|
||||
],
|
||||
};
|
||||
|
|
|
@ -23,11 +23,23 @@
|
|||
class="nav"
|
||||
@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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, onMounted, nextTick } from 'vue';
|
||||
|
||||
import events from '#/src/events.js';
|
||||
|
||||
|
@ -36,6 +48,9 @@ import Sidebar from '#/components/sidebar/sidebar.vue';
|
|||
import BottomNavigation from '#/components/footer/navigation.vue';
|
||||
|
||||
const content = ref(null);
|
||||
const feedback = ref(null);
|
||||
const feedbackContainer = ref(null);
|
||||
const feedbackBubble = ref(null);
|
||||
const showSidebar = ref(false);
|
||||
|
||||
function blur() {
|
||||
|
@ -44,6 +59,38 @@ function blur() {
|
|||
|
||||
onMounted(() => {
|
||||
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>
|
||||
|
||||
|
@ -102,9 +149,47 @@ onMounted(() => {
|
|||
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) {
|
||||
.nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.feedback-container {
|
||||
bottom: 4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
// `usePageContext` allows us to access `pageContext` in any Vue component.
|
||||
// See https://vike.dev/pageContext-anywhere
|
||||
|
||||
import { inject } from 'vue'
|
||||
import { inject } from 'vue';
|
||||
|
||||
export { usePageContext }
|
||||
export { setPageContext }
|
||||
const key = Symbol(); // eslint-disable-line symbol-description
|
||||
|
||||
const key = Symbol()
|
||||
|
||||
function usePageContext() {
|
||||
const pageContext = inject(key)
|
||||
return pageContext
|
||||
export function usePageContext() {
|
||||
const pageContext = inject(key);
|
||||
return pageContext;
|
||||
}
|
||||
|
||||
function setPageContext(app, pageContext) {
|
||||
app.provide(key, pageContext)
|
||||
export function setPageContext(app, 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 { unit } from 'mathjs';
|
||||
|
||||
import knex from './knex.js';
|
||||
import { searchApi } from './manticore.js';
|
||||
import { knexOwner as knex, knexManticore } from './knex.js';
|
||||
import { utilsApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
import { fetchCountriesByAlpha2 } from './countries.js';
|
||||
import { curateStash } from './stashes.js';
|
||||
|
||||
export function curateActor(actor, context = {}) {
|
||||
return {
|
||||
|
@ -58,6 +59,7 @@ export function curateActor(actor, context = {}) {
|
|||
createdAt: actor.created_at,
|
||||
updatedAt: actor.updated_at,
|
||||
likes: actor.stashed,
|
||||
stashes: context.stashes?.map((stash) => curateStash(stash)) || [],
|
||||
...context.append?.[actor.id],
|
||||
};
|
||||
}
|
||||
|
@ -73,8 +75,8 @@ export function sortActorsByGender(actors) {
|
|||
return genderActors;
|
||||
}
|
||||
|
||||
export async function fetchActorsById(actorIds, options = {}) {
|
||||
const [actors] = await Promise.all([
|
||||
export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
const [actors, stashes] = await Promise.all([
|
||||
knex('actors')
|
||||
.select(
|
||||
'actors.*',
|
||||
|
@ -93,10 +95,19 @@ export async function fetchActorsById(actorIds, options = {}) {
|
|||
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) {
|
||||
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) => {
|
||||
|
@ -107,7 +118,10 @@ export async function fetchActorsById(actorIds, options = {}) {
|
|||
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);
|
||||
|
||||
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) {
|
||||
const query = {
|
||||
bool: {
|
||||
|
@ -230,31 +268,7 @@ function buildQuery(filters) {
|
|||
return { query, expressions };
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export async function fetchActors(filters, rawOptions) {
|
||||
const options = curateOptions(rawOptions);
|
||||
async function queryManticoreJson(filters, options) {
|
||||
const { query, expressions } = buildQuery(filters);
|
||||
|
||||
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([
|
||||
fetchActorsById(actorIds),
|
||||
fetchCountriesByAlpha2(result.aggregations.countries.buckets.map((bucket) => bucket.key)),
|
||||
fetchActorsById(actorIds, {}, reqUser),
|
||||
fetchCountriesByAlpha2(result.aggregations.countries.map((bucket) => bucket.key)),
|
||||
]);
|
||||
|
||||
return {
|
||||
actors,
|
||||
countries,
|
||||
total: result.hits.total,
|
||||
total: result.total,
|
||||
limit: options.limit,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import config from 'config';
|
|||
import util from 'util'; /* eslint-disable-line no-unused-vars */
|
||||
|
||||
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 { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
|
||||
import { fetchTagsById } from './tags.js';
|
||||
|
@ -55,7 +55,10 @@ function curateScene(rawScene, assets) {
|
|||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
} : 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) => ({
|
||||
id: director.id,
|
||||
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([
|
||||
knex('releases').whereIn('releases.id', sceneIds),
|
||||
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) => {
|
||||
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 scenePhotos = photos.filter((photo) => photo.release_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, {
|
||||
channel: sceneChannel,
|
||||
|
@ -148,6 +159,7 @@ export async function fetchScenesById(sceneIds, reqUser) {
|
|||
poster: scenePoster,
|
||||
photos: scenePhotos,
|
||||
stashes: sceneStashes,
|
||||
actorStashes: sceneActorStashes,
|
||||
});
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
@ -171,6 +183,7 @@ function curateOptions(options) {
|
|||
};
|
||||
}
|
||||
|
||||
/*
|
||||
function buildQuery(filters = {}, options) {
|
||||
const query = {
|
||||
bool: {
|
||||
|
@ -215,23 +228,6 @@ function buildQuery(filters = {}, options) {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -262,16 +258,6 @@ function buildQuery(filters = {}, options) {
|
|||
query.bool.must.push({ equals: { stash_id: filters.stashId } });
|
||||
}
|
||||
|
||||
/* tag filter
|
||||
must_not: [
|
||||
{
|
||||
in: {
|
||||
'any(tag_ids)': [101, 180, 32],
|
||||
},
|
||||
},
|
||||
],
|
||||
*/
|
||||
|
||||
return { query, sort };
|
||||
}
|
||||
|
||||
|
@ -311,14 +297,6 @@ function buildAggregates(options) {
|
|||
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) {
|
||||
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])),
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
async function queryManticoreSql(filters, options, _reqUser) {
|
||||
const aggSize = config.database.manticore.maxAggregateSize;
|
||||
|
||||
const sqlQuery = knexManticore.raw(`
|
||||
:query:
|
||||
OPTION field_weights=(
|
||||
OPTION
|
||||
field_weights=(
|
||||
title_filtered=7,
|
||||
actors=10,
|
||||
tags=9,
|
||||
|
@ -391,6 +371,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||
scenes.channel_id as channel_id,
|
||||
scenes.network_id as network_id,
|
||||
scenes.effective_date as effective_date,
|
||||
scenes.stashed as stashed,
|
||||
scenes.created_at,
|
||||
created_at as stashed_at,
|
||||
weight() as _score
|
||||
|
@ -481,7 +462,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||
|
||||
const results = await utilsApi.sql(curatedSqlQuery);
|
||||
|
||||
console.log(results[0]);
|
||||
// console.log(results[0]);
|
||||
|
||||
const actorIds = results
|
||||
.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) {
|
||||
const options = curateOptions(rawOptions);
|
||||
|
||||
|
@ -527,10 +516,6 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
|||
const result = await queryManticoreSql(filters, options, reqUser);
|
||||
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 tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
|
||||
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
||||
|
@ -547,7 +532,7 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
|||
|
||||
console.time('fetch full');
|
||||
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');
|
||||
|
||||
return {
|
||||
|
|
|
@ -170,12 +170,26 @@ export async function refreshActorsView() {
|
|||
export async function stashActor(actorId, stashId, sessionUser) {
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
await knex('stashes_actors')
|
||||
const [stashed] = await knex('stashes_actors')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
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();
|
||||
|
||||
return fetchStashes('actor', actorId, sessionUser);
|
||||
|
@ -192,6 +206,25 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
|||
.where('stashes.user_id', sessionUser.id))
|
||||
.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();
|
||||
|
||||
return fetchStashes('actor', actorId, sessionUser);
|
||||
|
@ -200,12 +233,27 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
|||
export async function stashScene(sceneId, stashId, sessionUser) {
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
await knex('stashes_scenes')
|
||||
const [stashed] = await knex('stashes_scenes')
|
||||
.insert({
|
||||
stash_id: stash.id,
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -225,7 +273,7 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
|
|||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{ equals: { id: sceneId } },
|
||||
{ equals: { scene_id: sceneId } },
|
||||
{ equals: { stash_id: stashId } },
|
||||
{ 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { format } from 'date-fns';
|
||||
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 slugify from '../utils/slugify.js';
|
||||
|
@ -105,11 +105,11 @@ async function updateStashed(docs) {
|
|||
|
||||
const stashDoc = sceneStashes.map((stash) => ({
|
||||
replace: {
|
||||
index: 'movies_liked',
|
||||
index: 'scenes_stashed',
|
||||
id: stash.stashed_id,
|
||||
doc: {
|
||||
// ...doc.replace.doc,
|
||||
movie_id: doc.replace.id,
|
||||
scene_id: doc.replace.id,
|
||||
user_id: stash.user_id,
|
||||
},
|
||||
},
|
||||
|
@ -127,35 +127,6 @@ async function updateStashed(docs) {
|
|||
}
|
||||
|
||||
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 docs = scenes.map((scene) => {
|
||||
|
@ -165,7 +136,7 @@ async function init() {
|
|||
|
||||
return {
|
||||
replace: {
|
||||
index: 'movies',
|
||||
index: 'scenes',
|
||||
id: scene.id,
|
||||
doc: {
|
||||
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)),
|
||||
weight: query.weight?.split(',').map((weight) => Number(weight)),
|
||||
requireAvatar: query.avatar,
|
||||
stashId: Number(query.stashId) || null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -26,7 +27,7 @@ export async function fetchActorsApi(req, res) {
|
|||
page: Number(req.query.page) || 1,
|
||||
limit: Number(req.query.limit) || 120,
|
||||
order: req.query.order?.split('.') || ['likes', 'desc'],
|
||||
});
|
||||
}, req.user);
|
||||
|
||||
res.send({
|
||||
actors,
|
||||
|
|
|
@ -11,7 +11,7 @@ export async function curateScenesQuery(query) {
|
|||
actorIds: [query.actorId, ...(query.actors?.split(',') || []).map((identifier) => parseActorIdentifier(identifier)?.id)].filter(Boolean),
|
||||
tagIds: await getIdsBySlug([query.tagSlug, ...(query.tags?.split(',') || [])], 'tags'),
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ export async function unstashSceneApi(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);
|
||||
}
|
||||
|
|
|
@ -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