Compare commits
3 Commits
ffcb77ab45
...
5db4f18123
Author | SHA1 | Date |
---|---|---|
|
5db4f18123 | |
|
43949185c2 | |
|
e32a366fff |
|
@ -27,6 +27,7 @@
|
|||
"vue/no-reserved-component-names": 0,
|
||||
"no-tabs": "off",
|
||||
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
|
||||
"prefer-destructuring": 0,
|
||||
"template-curly-spacing": "off",
|
||||
"vue/html-indent": ["error", "tab"],
|
||||
"vue/multiline-html-element-content-newline": 0,
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
--text-light: #fff;
|
||||
|
||||
--link: #48f;
|
||||
--error: #f66;
|
||||
|
||||
--male: #0af;
|
||||
--female: #f0a;
|
||||
|
@ -59,4 +58,10 @@
|
|||
--disabled: #c20;
|
||||
--disabled-background: rgba(255, 0, 0, .1);
|
||||
--disabled-handle: var(--grey-light-10);
|
||||
|
||||
--error: #f66;
|
||||
--alert: #f00;
|
||||
--warn: #fa0;
|
||||
--success: #5c2;
|
||||
--notice: #25c;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
width="20.714647"
|
||||
height="19.141911"
|
||||
viewBox="0 0 20.714647 19.141911"
|
||||
id="svg1"
|
||||
sodipodi:docname="genders.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="12.621741"
|
||||
inkscape:cx="6.7344115"
|
||||
inkscape:cy="0"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1020"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<title
|
||||
id="title1">male</title>
|
||||
<path
|
||||
id="path1"
|
||||
d="M 15.714844 0 L 11.472656 4.2421875 L 12.886719 5.65625 L 14.714844 3.828125 L 14.714844 9.2441406 C 13.785707 9.4322314 12.900229 9.8868806 12.179688 10.607422 C 10.227367 12.559742 10.227367 15.725414 12.179688 17.677734 C 14.132008 19.630054 17.29768 19.630054 19.25 17.677734 C 21.20232 15.725414 21.20232 12.559742 19.25 10.607422 C 18.529459 9.8868806 17.643981 9.4322314 16.714844 9.2441406 L 16.714844 3.828125 L 18.542969 5.65625 L 19.957031 4.2421875 L 15.714844 0 z M 5 1.5703125 C 2.2390028 1.5703125 0 3.8093153 0 6.5703125 C 0 8.9893106 1.7180023 11.007704 4 11.470703 L 4 13.570312 L 1 13.570312 L 1 15.570312 L 4 15.570312 L 4 17.570312 L 6 17.570312 L 6 15.570312 L 9 15.570312 L 9 13.570312 L 6 13.570312 L 6 11.470703 C 8.2819977 11.007704 10 8.9893096 10 6.5703125 C 10 3.8093153 7.7609972 1.5703125 5 1.5703125 z M 5 3.2714844 C 6.8229982 3.2714844 8.3007812 4.7483143 8.3007812 6.5703125 C 8.3007812 8.3923102 6.8229982 9.8710938 5 9.8710938 C 3.1770018 9.8710938 1.6992187 8.3923112 1.6992188 6.5703125 C 1.6992188 4.7473143 3.1770018 3.2714844 5 3.2714844 z M 15.714844 10.841797 C 16.559394 10.841709 17.404301 11.164066 18.048828 11.808594 C 19.337883 13.097647 19.337883 15.187509 18.048828 16.476562 C 16.759774 17.765617 14.669913 17.765617 13.380859 16.476562 C 12.091805 15.187509 12.091805 13.097647 13.380859 11.808594 C 14.025033 11.16442 14.870294 10.841885 15.714844 10.841797 z " />
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>male</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 613 B After Width: | Height: | Size: 613 B |
|
@ -0,0 +1,257 @@
|
|||
<template>
|
||||
<div class="actors-container">
|
||||
<div class="actors-filters">
|
||||
<input
|
||||
v-model="search"
|
||||
type="search"
|
||||
placeholder="Filter actors"
|
||||
class="input input-inline actors-search"
|
||||
>
|
||||
|
||||
<div
|
||||
class="filter-sort noselect"
|
||||
@click="selectGender"
|
||||
>
|
||||
<div
|
||||
v-if="!selectedGender"
|
||||
class="gender-unselected"
|
||||
><Icon icon="genders" /></div>
|
||||
|
||||
<Gender
|
||||
v-else
|
||||
:gender="selectedGender"
|
||||
class="gender"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="order === 'name'"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'count'"
|
||||
>
|
||||
<Icon
|
||||
icon="sort-alpha-asc"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="order === 'count'"
|
||||
class="filter-sort order noselect"
|
||||
@click="order = 'name'"
|
||||
>
|
||||
<Icon
|
||||
icon="sort-numeric-desc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
v-for="(group, groupKey) in groupedActors"
|
||||
:key="groupKey"
|
||||
class="filter-items nolist"
|
||||
>
|
||||
<li
|
||||
v-for="(actor, index) in group"
|
||||
:key="`filter-actor-${actor.id}`"
|
||||
class="filter-item"
|
||||
:class="{ selected: filters.actors.includes(actor.id), first: groupKey === 'available' && index === 0 && filters.actors.length > 0 }"
|
||||
@click="emit('update', 'actors', [actor.id])"
|
||||
>
|
||||
<div
|
||||
class="filter-include"
|
||||
@click.stop="toggleActor(actor)"
|
||||
>
|
||||
<Icon
|
||||
icon="checkmark"
|
||||
class="filter-add"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
class="filter-remove"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="filter-name actor-name">{{ actor.name }}</span>
|
||||
|
||||
<span class="actor-details">
|
||||
<div class="actor-gender">
|
||||
<Gender
|
||||
:gender="actor.gender"
|
||||
class="gender"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span
|
||||
v-if="actor.count"
|
||||
class="actor-count"
|
||||
>{{ actor.count }}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
|
||||
import Gender from '#/components/actors/gender.vue';
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
actors: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update']);
|
||||
|
||||
const search = ref('');
|
||||
const searchRegexp = computed(() => new RegExp(search.value, 'i'));
|
||||
const selectedGender = ref(null);
|
||||
const order = ref('name');
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
const { actor: pageActor } = pageProps;
|
||||
|
||||
const groupedActors = computed(() => ({
|
||||
selected: props.actors.filter((actor) => props.filters.actors.includes(actor.id)),
|
||||
available: props.actors
|
||||
.filter((actor) => !props.filters.actors.includes(actor.id)
|
||||
&& actor.id !== pageActor?.id
|
||||
&& searchRegexp.value.test(actor.name)
|
||||
&& (!selectedGender.value || actor.gender === selectedGender.value))
|
||||
.sort((actorA, actorB) => {
|
||||
if (order.value === 'count') {
|
||||
return actorB.count - actorA.count;
|
||||
}
|
||||
|
||||
return actorA.name.localeCompare(actorB.name);
|
||||
}),
|
||||
}));
|
||||
|
||||
const genders = computed(() => [null, ...['female', 'male', 'transsexual', 'other'].filter((gender) => props.actors.some((actor) => actor.gender === gender))]);
|
||||
|
||||
function toggleActor(actor) {
|
||||
if (props.filters.actors.includes(actor.id)) {
|
||||
emit('update', 'actors', props.filters.actors.filter((actorId) => actorId !== actor.id));
|
||||
return;
|
||||
}
|
||||
|
||||
emit('update', 'actors', props.filters.actors.concat(actor.id));
|
||||
}
|
||||
|
||||
function selectGender() {
|
||||
console.log(genders.value);
|
||||
const genderIndex = genders.value.indexOf(selectedGender.value);
|
||||
|
||||
if (genderIndex >= genders.value.length - 1) {
|
||||
selectedGender.value = genders.value[0];
|
||||
return;
|
||||
}
|
||||
|
||||
selectedGender.value = genders.value[genderIndex + 1];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actors-container {
|
||||
box-shadow: inset 0 -1px 3px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.actors-filters {
|
||||
display: flex;
|
||||
border-bottom: solid 1px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.actors-search {
|
||||
flex-grow: 1;
|
||||
border-bottom: none;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.filter-sort {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
padding: 0 .5rem;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
color: var(--shadow);
|
||||
|
||||
&.order {
|
||||
padding: 0 1rem 0 .25rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--primary);
|
||||
|
||||
.icon {
|
||||
fill: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gender-unselected {
|
||||
.icon {
|
||||
fill: var(--shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-items {
|
||||
max-height: 15rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-item.first {
|
||||
border-top: solid 1px var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
height: 1rem;
|
||||
align-items: stretch;
|
||||
padding: .25rem .75rem .25rem .25rem;
|
||||
}
|
||||
|
||||
.actor-name {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.actor-details {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.actor-gender {
|
||||
width: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.gender {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.actor-count {
|
||||
width: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 .25rem;
|
||||
overflow: hidden;
|
||||
color: var(--shadow-weak-10);
|
||||
font-size: .9rem;
|
||||
}
|
||||
</style>
|
|
@ -7,7 +7,7 @@
|
|||
v-if="!filters.country"
|
||||
v-model="countryQuery"
|
||||
type="search"
|
||||
placeholder="Filter country"
|
||||
placeholder="Filter countries"
|
||||
class="input input-inline countries-search"
|
||||
>
|
||||
|
||||
|
|
|
@ -67,6 +67,120 @@ function toggleFilters(state) {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.filter-mode {
|
||||
width: 100%;
|
||||
color: var(--shadow);
|
||||
background: none;
|
||||
padding: .75rem;
|
||||
margin: 0 0 .5rem 0;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-bottom: solid 1px var(--shadow-hint);
|
||||
|
||||
option {
|
||||
color: var(--text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.filter-clear {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .5rem 1rem;
|
||||
color: var(--shadow-weak-10);
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-weak-30);
|
||||
margin: 0 0 0 1rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--shadow);
|
||||
|
||||
.icon {
|
||||
fill: var(--shadow-weak-10);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
background: var(--shadow-hint);
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: var(--alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-items .filter-item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
&:hover {
|
||||
background: var(--shadow-weak-30);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
.filter-include {
|
||||
.filter-add {
|
||||
fill: var(--success);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.filter-add {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filter-remove {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-name {
|
||||
min-width: 8rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
padding: .25rem .75rem .25rem 1rem;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.filter-include {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
width: 1rem;
|
||||
padding: 0 .75rem;
|
||||
fill: var(--shadow-weak-30);
|
||||
}
|
||||
|
||||
.filter-remove {
|
||||
display: none;
|
||||
fill: var(--alert);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-include:hover,
|
||||
.filter-name:hover {
|
||||
background: var(--shadow-weak-30);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.filters-frame {
|
||||
position: relative;
|
||||
|
@ -88,7 +202,7 @@ function toggleFilters(state) {
|
|||
flex-grow: 1;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem 0;
|
||||
padding: 0 0 .5rem 0;
|
||||
background: var(--background);
|
||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||
|
||||
|
|
|
@ -1,60 +1,180 @@
|
|||
<template>
|
||||
<div class="page">
|
||||
<nav class="scopes">
|
||||
<Link
|
||||
href="/updates/latest"
|
||||
class="scope nolink"
|
||||
:active="scope === 'latest'"
|
||||
>Latest</Link>
|
||||
<Filters v-if="showFilters">
|
||||
<ActorsFilter
|
||||
:filters="filters"
|
||||
:actors="actors"
|
||||
@update="updateFilter"
|
||||
/>
|
||||
</Filters>
|
||||
|
||||
<Link
|
||||
href="/updates/upcoming"
|
||||
class="scope nolink"
|
||||
:active="scope === 'upcoming'"
|
||||
>Upcoming</Link>
|
||||
|
||||
<Link
|
||||
href="/updates/new"
|
||||
class="scope nolink"
|
||||
:active="scope === 'new'"
|
||||
>New</Link>
|
||||
</nav>
|
||||
|
||||
<ul class="scenes nolist">
|
||||
<li
|
||||
v-for="scene in scenes"
|
||||
:key="scene.id"
|
||||
<div class="scenes-container">
|
||||
<div
|
||||
v-if="showMeta"
|
||||
class="scenes-header"
|
||||
>
|
||||
<Scene :scene="scene" />
|
||||
</li>
|
||||
</ul>
|
||||
<div class="meta">{{ total }} results</div>
|
||||
</div>
|
||||
|
||||
<Pagination />
|
||||
<nav class="scopes">
|
||||
<Link
|
||||
:href="getPath('latest')"
|
||||
class="scope nolink"
|
||||
:active="scope === 'latest'"
|
||||
>Latest</Link>
|
||||
|
||||
<Link
|
||||
:href="getPath('upcoming')"
|
||||
class="scope nolink"
|
||||
:active="scope === 'upcoming'"
|
||||
>Upcoming</Link>
|
||||
|
||||
<Link
|
||||
:href="getPath('new')"
|
||||
class="scope nolink"
|
||||
:active="scope === 'new'"
|
||||
>New</Link>
|
||||
</nav>
|
||||
|
||||
<ul class="scenes nolist">
|
||||
<li
|
||||
v-for="scene in scenes"
|
||||
:key="scene.id"
|
||||
>
|
||||
<Scene :scene="scene" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Pagination />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { ref, inject } from 'vue';
|
||||
import { parse } from 'path-to-regexp';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
import { get } from '#/src/api.js';
|
||||
import events from '#/src/events.js';
|
||||
|
||||
import Filters from '#/components/filters/filters.vue';
|
||||
import ActorsFilter from '#/components/filters/actors.vue';
|
||||
import Scene from './tile.vue';
|
||||
import Pagination from '../pagination/pagination.vue';
|
||||
|
||||
const { routeParams } = inject('pageContext');
|
||||
const { scope } = routeParams;
|
||||
|
||||
defineProps({
|
||||
scenes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
showFilters: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showMeta: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { pageProps, routeParams, urlParsed } = inject('pageContext');
|
||||
const { scope } = routeParams;
|
||||
|
||||
const {
|
||||
actor,
|
||||
} = pageProps;
|
||||
|
||||
const scenes = ref(pageProps.scenes);
|
||||
const actors = ref(pageProps.actors);
|
||||
|
||||
const currentPage = ref(Number(routeParams.page));
|
||||
const total = ref(Number(pageProps.total));
|
||||
|
||||
const filters = ref({
|
||||
actors: urlParsed.search.actors?.split(',').filter(Boolean).map((actorId) => Number(actorId)) || [],
|
||||
});
|
||||
|
||||
function getPath(targetScope, preserveQuery) {
|
||||
const path = parse(routeParams.path)
|
||||
.map((segment) => {
|
||||
if (segment.name === 'scope') {
|
||||
return targetScope;
|
||||
}
|
||||
|
||||
if (segment.name === 'page') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return routeParams[segment.name] || segment;
|
||||
})
|
||||
.join('/');
|
||||
|
||||
if (preserveQuery && urlParsed.searchOriginal) {
|
||||
return `${path}${urlParsed.searchOriginal}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async function search(resetPage = true) {
|
||||
if (resetPage) {
|
||||
currentPage.value = 1;
|
||||
}
|
||||
|
||||
const query = {};
|
||||
|
||||
const res = await get('/scenes', {
|
||||
...query,
|
||||
actors: [actor?.id, filters.value.actors].filter(Boolean).join(','), // if we're on an actor page, actor ID needs to be included
|
||||
scope,
|
||||
page: currentPage.value, // client uses param rather than query pagination
|
||||
});
|
||||
|
||||
scenes.value = res.scenes;
|
||||
actors.value = res.actors;
|
||||
total.value = res.total;
|
||||
|
||||
console.log(scenes.value);
|
||||
|
||||
events.emit('scrollUp');
|
||||
|
||||
navigate(getPath(scope, false), {
|
||||
...query,
|
||||
actors: filters.value.actors.join(',') || undefined, // don't include actor page ID in query, already a parameter
|
||||
}, { redirect: false });
|
||||
}
|
||||
|
||||
function updateFilter(prop, value, reload = true) {
|
||||
filters.value[prop] = value;
|
||||
|
||||
if (reload) {
|
||||
search();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: flex;
|
||||
background: var(--background-base-10);
|
||||
}
|
||||
|
||||
.scenes-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem 0 .25rem 2rem;
|
||||
}
|
||||
|
||||
.scenes-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scenes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
|
||||
|
|
|
@ -141,6 +141,7 @@ defineProps({
|
|||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: .5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.row {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
{
|
||||
"name": "traxxx-web",
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.4.2",
|
||||
"version": "0.4.3",
|
||||
"dependencies": {
|
||||
"@brillout/json-serializer": "^0.5.8",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"server:prod": "cross-env NODE_ENV=production node ./src/web/server"
|
||||
},
|
||||
"dependencies": {
|
||||
"@brillout/json-serializer": "^0.5.8",
|
||||
"@floating-ui/dom": "^1.5.3",
|
||||
"@floating-ui/vue": "^1.0.2",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
|
@ -45,5 +46,5 @@
|
|||
"postcss-custom-media": "^10.0.2",
|
||||
"postcss-nesting": "^12.0.2"
|
||||
},
|
||||
"version": "0.4.2"
|
||||
"version": "0.4.3"
|
||||
}
|
||||
|
|
|
@ -1,11 +1,45 @@
|
|||
<template>
|
||||
<div class="page">
|
||||
<h2>Actor</h2>
|
||||
<div class="bio">
|
||||
<img
|
||||
v-if="actor.avatar"
|
||||
:src="actor.avatar.isS3 ? `https://cdndev.traxxx.me/${actor.avatar.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
|
||||
:style="{ 'background-image': actor.avatar.isS3 ? `url(https://cdndev.traxxx.me/${actor.avatar.lazy})` : `url(/media/${actor.avatar.lazy})` }"
|
||||
class="avatar"
|
||||
>
|
||||
|
||||
<h2 class="name">{{ actor.name }}</h2>
|
||||
</div>
|
||||
|
||||
<Scenes />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
const { routeParams } = inject('pageContext');
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const { pageProps } = pageContext;
|
||||
|
||||
const { actor } = pageProps;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bio {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
background: var(--grey-dark-40);
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
height: 10rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
import { fetchActorsById } from '#/src/actors.js';
|
||||
import { fetchScenes } from '#/src/scenes.js';
|
||||
import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [actor] = await fetchActorsById([Number(pageContext.routeParams.actorId)]);
|
||||
|
||||
const {
|
||||
scenes,
|
||||
actors,
|
||||
total,
|
||||
limit,
|
||||
} = await fetchScenes(curateScenesQuery({
|
||||
...pageContext.urlQuery,
|
||||
scope: pageContext.routeParams.scope || 'latest',
|
||||
actorId: Number(pageContext.routeParams.actorId),
|
||||
}), {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: true,
|
||||
});
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: actor.name,
|
||||
pageProps: {
|
||||
actor,
|
||||
actors,
|
||||
scenes,
|
||||
total,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1 +1,23 @@
|
|||
export default '/actor/@actorId/*';
|
||||
import { match } from 'path-to-regexp';
|
||||
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||
|
||||
const path = '/actor/:actorId/:actorSlug?/:scope?/:page?';
|
||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||
|
||||
export default (pageContext) => {
|
||||
const matched = urlMatch(pageContext.urlPathname);
|
||||
|
||||
if (matched) {
|
||||
return {
|
||||
routeParams: {
|
||||
actorId: matched.params.actorId,
|
||||
actorSlug: matched.params.actorSlug,
|
||||
scope: matched.params.scope || 'latest',
|
||||
page: matched.params.page || '1',
|
||||
path,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<div>
|
||||
<Scenes
|
||||
:scenes="scenes"
|
||||
:show-filters="false"
|
||||
:show-meta="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -9,7 +11,7 @@
|
|||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import Scenes from '../../components/scenes/scenes.vue';
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
|
||||
const { pageProps } = inject('pageContext');
|
||||
const { scenes } = pageProps;
|
||||
|
|
|
@ -1,21 +1,13 @@
|
|||
import { fetchLatest, fetchUpcoming, fetchNew } from '#/src/scenes.js';
|
||||
|
||||
async function fetchScenes(scope, page, limit) {
|
||||
if (scope === 'new') {
|
||||
return fetchNew(page, { limit });
|
||||
}
|
||||
|
||||
if (scope === 'upcoming') {
|
||||
return fetchUpcoming(page, { limit });
|
||||
}
|
||||
|
||||
return fetchLatest(page, { limit });
|
||||
}
|
||||
import { fetchScenes } from '#/src/scenes.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const { scenes, limit, total } = await fetchScenes(pageContext.routeParams.scope, Number(pageContext.routeParams.page) || 1, Number(pageContext.urlParsed.search.limit) || 30);
|
||||
|
||||
// console.log(scenes);
|
||||
const { scenes, limit, total } = await fetchScenes({
|
||||
scope: pageContext.routeParams.scope || 'latest',
|
||||
}, {
|
||||
page: Number(pageContext.routeParams.page) || 1,
|
||||
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||
aggregate: false,
|
||||
});
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
|
|
|
@ -31,6 +31,7 @@ export function curateActor(actor, context = {}) {
|
|||
isS3: actor.avatar.is_s3,
|
||||
},
|
||||
likes: actor.stashed,
|
||||
...context.append?.[actor.id],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -45,13 +46,22 @@ export function sortActorsByGender(actors) {
|
|||
return genderActors;
|
||||
}
|
||||
|
||||
export async function fetchActorsById(actorIds) {
|
||||
export async function fetchActorsById(actorIds, options = {}) {
|
||||
const [actors] = await Promise.all([
|
||||
knex('actors_meta')
|
||||
.select('actors_meta.*')
|
||||
.whereIn('actors_meta.id', actorIds),
|
||||
.whereIn('actors_meta.id', actorIds)
|
||||
.modify((builder) => {
|
||||
if (options.order) {
|
||||
builder.orderBy(...options.order);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
if (options.order) {
|
||||
return actors.map((actorEntry) => curateActor(actorEntry, { append: options.append }));
|
||||
}
|
||||
|
||||
const curatedActors = actorIds.map((actorId) => {
|
||||
const actor = actors.find((actorEntry) => actorEntry.id === actorId);
|
||||
|
||||
|
@ -60,7 +70,7 @@ export async function fetchActorsById(actorIds) {
|
|||
return null;
|
||||
}
|
||||
|
||||
return curateActor(actor);
|
||||
return curateActor(actor, { append: options.append });
|
||||
}).filter(Boolean);
|
||||
|
||||
return curatedActors;
|
||||
|
|
10
src/api.js
10
src/api.js
|
@ -1,3 +1,5 @@
|
|||
import { parse } from '@brillout/json-serializer/parse';
|
||||
|
||||
const postHeaders = {
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin',
|
||||
|
@ -18,7 +20,7 @@ function getQuery(data) {
|
|||
|
||||
export async function get(path, query = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`);
|
||||
const body = await res.json();
|
||||
const body = parse(await res.text());
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
|
@ -38,7 +40,7 @@ export async function post(path, data, { query } = {}) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
const body = parse(await res.text());
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
|
@ -58,7 +60,7 @@ export async function patch(path, data, { query } = {}) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
const body = parse(await res.text());
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
|
@ -78,7 +80,7 @@ export async function del(path, { data, query } = {}) {
|
|||
return null;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
const body = parse(await res.text());
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
|
|
180
src/scenes.js
180
src/scenes.js
|
@ -1,7 +1,7 @@
|
|||
import knex from './knex.js';
|
||||
import { searchApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
import { curateActor, sortActorsByGender } from './actors.js';
|
||||
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
|
||||
|
||||
function curateMedia(media) {
|
||||
if (!media) {
|
||||
|
@ -67,16 +67,6 @@ function curateScene(rawScene, assets) {
|
|||
};
|
||||
}
|
||||
|
||||
function curateOptions(options) {
|
||||
if (options?.limit > 100) {
|
||||
throw new HttpError('Limit must be <= 100', 400);
|
||||
}
|
||||
|
||||
return {
|
||||
limit: options.limit || 30,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchScenesById(sceneIds) {
|
||||
const [scenes, channels, actors, directors, tags, posters, photos] = await Promise.all([
|
||||
knex('releases').whereIn('id', sceneIds),
|
||||
|
@ -114,7 +104,8 @@ export async function fetchScenesById(sceneIds) {
|
|||
.select('id', 'slug', 'name', 'release_id')
|
||||
.whereNotNull('tags.id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id'),
|
||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||
.orderBy('priority', 'desc'),
|
||||
knex('releases_posters')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
||||
|
@ -148,97 +139,118 @@ export async function fetchScenesById(sceneIds) {
|
|||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
export async function fetchLatest(page, rawOptions) {
|
||||
const { limit } = curateOptions(rawOptions);
|
||||
function curateOptions(options) {
|
||||
if (options?.limit > 100) {
|
||||
throw new HttpError('Limit must be <= 100', 400);
|
||||
}
|
||||
|
||||
const result = await searchApi.search({
|
||||
index: 'scenes',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
range: {
|
||||
effective_date: {
|
||||
lte: Math.round(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
/*
|
||||
must_not: [
|
||||
{
|
||||
in: {
|
||||
'any(tag_ids)': [101, 180, 32],
|
||||
},
|
||||
},
|
||||
],
|
||||
*/
|
||||
return {
|
||||
limit: options?.limit || 30,
|
||||
page: Number(options?.page) || 1,
|
||||
aggregate: options.aggregate ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
function buildQuery(filters = {}) {
|
||||
const query = {
|
||||
bool: {
|
||||
must: [],
|
||||
},
|
||||
};
|
||||
|
||||
let sort = [{ effective_date: 'desc' }];
|
||||
|
||||
if (!filters.scope || filters.scope === 'latest') {
|
||||
query.bool.must.push({
|
||||
range: {
|
||||
effective_date: {
|
||||
lte: Math.round(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.scope === 'upcoming') {
|
||||
query.bool.must.push({
|
||||
range: {
|
||||
effective_date: {
|
||||
gt: Math.round(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sort = [{ effective_date: 'asc' }];
|
||||
}
|
||||
|
||||
if (filters.scope === 'new') {
|
||||
sort = [{ created_at: 'desc' }, { effective_date: 'asc' }];
|
||||
}
|
||||
|
||||
if (filters.actorIds) {
|
||||
console.log('actor ids', filters.actorIds);
|
||||
|
||||
filters.actorIds.forEach((actorId) => {
|
||||
query.bool.must.push({
|
||||
equals: {
|
||||
'any(actor_ids)': actorId,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* tag filter
|
||||
must_not: [
|
||||
{
|
||||
in: {
|
||||
'any(tag_ids)': [101, 180, 32],
|
||||
},
|
||||
},
|
||||
limit,
|
||||
offset: (page - 1) * limit,
|
||||
sort: [{ effective_date: 'desc' }],
|
||||
});
|
||||
],
|
||||
*/
|
||||
|
||||
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
||||
const scenes = await fetchScenesById(sceneIds);
|
||||
console.log(query.bool.must);
|
||||
|
||||
return {
|
||||
scenes,
|
||||
total: result.hits.total,
|
||||
limit,
|
||||
};
|
||||
return { query, sort };
|
||||
}
|
||||
|
||||
export async function fetchUpcoming(page, rawOptions) {
|
||||
const { limit } = curateOptions(rawOptions);
|
||||
export async function fetchScenes(filters, rawOptions) {
|
||||
const options = curateOptions(rawOptions);
|
||||
const { query, sort } = buildQuery(filters);
|
||||
|
||||
console.log(filters);
|
||||
|
||||
const result = await searchApi.search({
|
||||
index: 'scenes',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
range: {
|
||||
effective_date: {
|
||||
gt: Math.round(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
query,
|
||||
limit: options.limit,
|
||||
offset: (options.page - 1) * options.limit,
|
||||
sort,
|
||||
...(options.aggregate && {
|
||||
aggs: {
|
||||
actorIds: {
|
||||
terms: {
|
||||
field: 'actor_ids',
|
||||
size: 5000,
|
||||
},
|
||||
],
|
||||
// sort: [{ doc_count: { order: 'asc' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
limit,
|
||||
offset: (page - 1) * limit,
|
||||
sort: [{ effective_date: 'asc' }],
|
||||
}),
|
||||
});
|
||||
|
||||
const actorCounts = result.aggregations?.actorIds && Object.fromEntries(result.aggregations?.actorIds?.buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||
|
||||
const [actors] = await Promise.all([
|
||||
result.aggregations?.actorIds ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [],
|
||||
]);
|
||||
|
||||
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
||||
const scenes = await fetchScenesById(sceneIds);
|
||||
|
||||
return {
|
||||
scenes,
|
||||
actors,
|
||||
total: result.hits.total,
|
||||
limit,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchNew(page, rawOptions) {
|
||||
const { limit } = curateOptions(rawOptions);
|
||||
|
||||
const result = await searchApi.search({
|
||||
index: 'scenes',
|
||||
limit,
|
||||
offset: (page - 1) * limit,
|
||||
sort: [{ created_at: 'desc' }, { effective_date: 'asc' }],
|
||||
});
|
||||
|
||||
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
||||
const scenes = await fetchScenesById(sceneIds);
|
||||
|
||||
return {
|
||||
scenes,
|
||||
total: result.hits.total,
|
||||
limit,
|
||||
limit: options.limit,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { stringify } from '@brillout/json-serializer/stringify';
|
||||
|
||||
import { fetchScenes } from '../scenes.js';
|
||||
|
||||
export function curateScenesQuery(query) {
|
||||
return {
|
||||
scope: query.scope || 'latest',
|
||||
actorIds: [query.actorId, ...(query.actors?.split(',') || [])].filter(Boolean).map((actorId) => Number(actorId)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchScenesApi(req, res) {
|
||||
const {
|
||||
scenes,
|
||||
actors,
|
||||
limit,
|
||||
total,
|
||||
} = await fetchScenes(curateScenesQuery(req.query), {
|
||||
page: Number(req.query.page) || 1,
|
||||
limit: Number(req.query.limit) || 30,
|
||||
});
|
||||
|
||||
res.send(stringify({
|
||||
scenes,
|
||||
actors,
|
||||
limit,
|
||||
total,
|
||||
}));
|
||||
}
|
|
@ -20,7 +20,8 @@ import { renderPage } from 'vike/server'; // eslint-disable-line import/extensio
|
|||
|
||||
// import root from './root.js';
|
||||
|
||||
import { fetchActorsApi } from './actors.js'; // eslint-disable-line import/extensions
|
||||
import { fetchScenesApi } from './scenes.js';
|
||||
import { fetchActorsApi } from './actors.js';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
|
@ -60,6 +61,8 @@ async function startServer() {
|
|||
|
||||
router.get('/api/actors', fetchActorsApi);
|
||||
|
||||
router.get('/api/scenes', fetchScenesApi);
|
||||
|
||||
// ...
|
||||
// Other middlewares (e.g. some RPC middleware such as Telefunc)
|
||||
// ...
|
||||
|
@ -75,6 +78,10 @@ async function startServer() {
|
|||
const pageContext = await renderPage(pageContextInit);
|
||||
const { httpResponse } = pageContext;
|
||||
|
||||
if (pageContext.errorWhileRendering) {
|
||||
console.error(pageContext.errorWhileRendering);
|
||||
}
|
||||
|
||||
if (!httpResponse) {
|
||||
next();
|
||||
return;
|
||||
|
|
Loading…
Reference in New Issue