Added actor profile revisions.
This commit is contained in:
parent
b5bef49f73
commit
3967745fb3
|
@ -2,3 +2,6 @@
|
||||||
path = static
|
path = static
|
||||||
ignore = all
|
ignore = all
|
||||||
url = git@unknown.name:DebaucheryLibrarian/traxxx-assets.git
|
url = git@unknown.name:DebaucheryLibrarian/traxxx-assets.git
|
||||||
|
[submodule "common"]
|
||||||
|
path = common
|
||||||
|
url = git@unknown.name:DebaucheryLibrarian/traxxx-common.git
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 40011a62dae9da8deda71e9f8daf39665a8b7958
|
|
@ -136,13 +136,32 @@
|
||||||
class="bio-item figure"
|
class="bio-item figure"
|
||||||
>
|
>
|
||||||
<dfn class="bio-label"><Icon icon="ruler" />Figure</dfn>
|
<dfn class="bio-label"><Icon icon="ruler" />Figure</dfn>
|
||||||
|
<span class="bio-value">{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
v-if="!actor.naturalBoobs || !actor.naturalButt"
|
||||||
|
class="bio-item augmentations"
|
||||||
|
>
|
||||||
|
<dfn class="bio-label"><Icon icon="magic-wand2" />Augmentations</dfn>
|
||||||
|
|
||||||
<span class="bio-value">
|
<span class="bio-value">
|
||||||
<Icon
|
<div
|
||||||
v-if="actor.naturalBoobs === false"
|
v-if="!actor.naturalBoobs"
|
||||||
:title="'Enhanced boobs'"
|
:title="[actor.boobsVolume, augmentationMap[actor.boobsPlacement] || actor.boobsPlacement, augmentationMap[actor.boobsImplant] || actor.boobsImplant].filter(Boolean).join(' ')"
|
||||||
icon="magic-wand2"
|
class="augmentations-section"
|
||||||
class="enhanced"
|
>Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">: </template>
|
||||||
/>{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
|
<template v-if="actor.boobsVolume">{{ actor.boobsVolume }}cc</template>
|
||||||
|
<template v-if="actor.boobsImplant"> {{ augmentationMap[actor.boobsImplant] || actor.boobsImplant }}</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!actor.naturalButt"
|
||||||
|
class="augmentations-section"
|
||||||
|
>Butt<template v-if="actor.buttVolume || actor.buttImplant">: </template>
|
||||||
|
<template v-if="actor.buttVolume">{{ actor.buttVolume }}cc</template>
|
||||||
|
<template v-if="actor.buttImplant"> {{ augmentationMap[actor.buttImplant] || actor.buttImplant }}</template>
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
@ -240,6 +259,21 @@
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="bio-item updated hideable">Updated {{ formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm') }}, ID: {{ actor.id }}</li>
|
<li class="bio-item updated hideable">Updated {{ formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm') }}, ID: {{ actor.id }}</li>
|
||||||
|
|
||||||
|
<li class="bio-item actor-actions">
|
||||||
|
<a
|
||||||
|
v-if="user && user.role !== 'user'"
|
||||||
|
:href="`/actor/edit/${actor.id}/${actor.slug}`"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>Edit bio</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="`/actor/revisions/${actor.id}/${actor.slug}`"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>Revisions</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="descriptions-container">
|
<div class="descriptions-container">
|
||||||
|
@ -294,13 +328,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, inject } from 'vue';
|
||||||
|
|
||||||
import getPath from '#/src/get-path.js';
|
import getPath from '#/src/get-path.js';
|
||||||
import { formatDate } from '#/utils/format.js';
|
import { formatDate } from '#/utils/format.js';
|
||||||
|
|
||||||
const expanded = ref(false);
|
const expanded = ref(false);
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const user = pageContext.user;
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
actor: {
|
actor: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -326,6 +363,17 @@ const showExpand = [
|
||||||
'weight',
|
'weight',
|
||||||
].some((attribute) => !!props.actor[attribute]);
|
].some((attribute) => !!props.actor[attribute]);
|
||||||
|
|
||||||
|
const augmentationMap = {
|
||||||
|
bbl: 'BBL',
|
||||||
|
fat: 'fat transfer',
|
||||||
|
lift: 'direct lift',
|
||||||
|
lipo: 'lipo without BBL',
|
||||||
|
filler: 'filler',
|
||||||
|
mms: 'MMS',
|
||||||
|
over: 'over-muscle',
|
||||||
|
under: 'under-muscle',
|
||||||
|
};
|
||||||
|
|
||||||
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||||
.filter((profile) => !!profile.description)
|
.filter((profile) => !!profile.description)
|
||||||
.map((profile) => [profile.descriptionHash, {
|
.map((profile) => [profile.descriptionHash, {
|
||||||
|
@ -519,6 +567,11 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||||
content: ',\00a0';
|
content: ',\00a0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.augmentations .bio-value {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.updated {
|
.updated {
|
||||||
color: var(--highlight-weak-20);
|
color: var(--highlight-weak-20);
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
|
@ -638,6 +691,16 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actor-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
font-weight: normal;
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--highlight-strong-20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media(--big) {
|
@media(--big) {
|
||||||
.descriptions-container {
|
.descriptions-container {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -4,10 +4,18 @@
|
||||||
<ul class="nav-items nolist">
|
<ul class="nav-items nolist">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
href="/admin/revisions"
|
href="/admin/revisions/scenes"
|
||||||
class="nav-link nolink"
|
class="nav-link nolink"
|
||||||
:class="{ active: pageContext.routeParams.section === 'revisions' }"
|
:class="{ active: pageContext.routeParams.section === 'revisions' && pageContext.routeParams.domain === 'scenes' }"
|
||||||
>Revisions</a>
|
>Scene Revisions</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
href="/admin/revisions/actors"
|
||||||
|
class="nav-link nolink"
|
||||||
|
:class="{ active: pageContext.routeParams.section === 'revisions' && pageContext.routeParams.domain === 'actors' }"
|
||||||
|
>Actor Revisions</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -22,8 +30,6 @@
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
// console.log(pageContext);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -36,7 +42,14 @@ const pageContext = inject('pageContext');
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1rem 1rem 0 1rem;
|
padding: 1rem 1rem .75rem 1rem;
|
||||||
|
border-bottom: solid 1px var(--shadow-weak-30);
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-items {
|
||||||
|
display: flex;
|
||||||
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
|
|
|
@ -27,16 +27,16 @@
|
||||||
<template v-if="context === 'admin' || expanded.has(rev.id)">
|
<template v-if="context === 'admin' || expanded.has(rev.id)">
|
||||||
<div class="rev-header">
|
<div class="rev-header">
|
||||||
<a
|
<a
|
||||||
:href="`/scene/${rev.sceneId}`"
|
:href="`/${domain.slice(0, -1)}/${rev.sceneId || rev.actorId}/${rev.base.slug}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="rev-link rev-scene nolink noshrink"
|
class="rev-link rev-scene nolink noshrink"
|
||||||
>{{ rev.sceneId }}@{{ rev.hash.slice(0, 6) }}</a>
|
>{{ rev.sceneId || rev.actorId }}@{{ rev.hash.slice(0, 6) }}</a>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="`/scene/${rev.sceneId}`"
|
:href="`/${domain.slice(0, -1)}/${rev.sceneId || rev.actorId}/${rev.base.slug}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="rev-link rev-title nolink ellipsis"
|
class="rev-link rev-title nolink ellipsis"
|
||||||
>{{ rev.base.title }}</a>
|
>{{ rev.base.title || rev.base.name }}</a>
|
||||||
|
|
||||||
<div class="rev-details noshrink">
|
<div class="rev-details noshrink">
|
||||||
<a
|
<a
|
||||||
|
@ -219,6 +219,7 @@ defineProps({
|
||||||
|
|
||||||
const pageContext = inject('pageContext');
|
const pageContext = inject('pageContext');
|
||||||
const revisions = ref(pageContext.pageProps.revisions);
|
const revisions = ref(pageContext.pageProps.revisions);
|
||||||
|
const domain = pageContext.routeParams.domain;
|
||||||
|
|
||||||
const actors = ref(pageContext.pageProps.actors);
|
const actors = ref(pageContext.pageProps.actors);
|
||||||
const tags = ref(pageContext.pageProps.tags);
|
const tags = ref(pageContext.pageProps.tags);
|
||||||
|
@ -241,6 +242,8 @@ const mappedKeys = {
|
||||||
|
|
||||||
const dateKeys = [
|
const dateKeys = [
|
||||||
'date',
|
'date',
|
||||||
|
'dateOfBirth',
|
||||||
|
'dateOfDeath',
|
||||||
'productionDate',
|
'productionDate',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
];
|
];
|
||||||
|
@ -307,7 +310,7 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async function reloadRevisions() {
|
async function reloadRevisions() {
|
||||||
const updatedRevisions = await get('/revisions', {
|
const updatedRevisions = await get(`/revisions/${domain}`, {
|
||||||
isFinalized: showReviewed.value ? undefined : false,
|
isFinalized: showReviewed.value ? undefined : false,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
});
|
});
|
||||||
|
@ -322,12 +325,12 @@ async function reviewRevision(revision, isApproved) {
|
||||||
reviewedRevisions.value.add(revision.id);
|
reviewedRevisions.value.add(revision.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await post(`/revisions/${revision.id}/reviews`, {
|
await post(`/revisions/${domain}/${revision.id}/reviews`, {
|
||||||
isApproved,
|
isApproved,
|
||||||
feedback: feedbacks.value[revision.id],
|
feedback: feedbacks.value[revision.id],
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedRevision = await get(`/revisions/${revision.id}`, {
|
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {
|
||||||
revisionId: revision.id,
|
revisionId: revision.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"mathjs": "^12.2.1",
|
"mathjs": "^12.2.1",
|
||||||
"merkle-json": "^2.6.0",
|
"merkle-json": "^2.6.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"object.omit": "^3.0.0",
|
"object.omit": "^3.0.0",
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
"mathjs": "^12.2.1",
|
"mathjs": "^12.2.1",
|
||||||
"merkle-json": "^2.6.0",
|
"merkle-json": "^2.6.0",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"nanoid": "^5.0.4",
|
"nanoid": "^5.0.4",
|
||||||
"object.omit": "^3.0.0",
|
"object.omit": "^3.0.0",
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
import { redirect, render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||||
|
|
||||||
import { fetchActorsById } from '#/src/actors.js';
|
import { fetchActorsById } from '#/src/actors.js';
|
||||||
import { fetchScenes } from '#/src/scenes.js';
|
import { fetchScenes } from '#/src/scenes.js';
|
||||||
import { fetchMovies } from '#/src/movies.js';
|
import { fetchMovies } from '#/src/movies.js';
|
||||||
import { curateScenesQuery } from '#/src/web/scenes.js';
|
import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||||
import { curateMoviesQuery } from '#/src/web/movies.js';
|
import { curateMoviesQuery } from '#/src/web/movies.js';
|
||||||
|
import { fetchCountries } from '#/src/countries.js';
|
||||||
|
|
||||||
async function fetchReleases(pageContext) {
|
async function fetchReleases(pageContext) {
|
||||||
if (pageContext.routeParams.domain === 'movies') {
|
if (pageContext.routeParams.domain === 'movies') {
|
||||||
|
@ -33,9 +34,16 @@ async function fetchReleases(pageContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
const [[actor], actorReleases] = await Promise.all([
|
const isEditing = pageContext._pageId === '/pages/actors/@actorId/edit';
|
||||||
|
|
||||||
|
if (isEditing && !pageContext.user) {
|
||||||
|
throw redirect(`/login?r=${encodeURIComponent(pageContext.urlOriginal)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [[actor], actorReleases, countries] = await Promise.all([
|
||||||
fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user),
|
fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user),
|
||||||
fetchReleases(pageContext),
|
fetchReleases(pageContext),
|
||||||
|
isEditing && fetchCountries(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
|
@ -44,9 +52,12 @@ export async function onBeforeRender(pageContext) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
title: actor.name,
|
title: isEditing
|
||||||
|
? `Editing '${actor.name}'`
|
||||||
|
: actor.name,
|
||||||
pageProps: {
|
pageProps: {
|
||||||
actor,
|
actor,
|
||||||
|
countries,
|
||||||
...actorReleases,
|
...actorReleases,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { match } from 'path-to-regexp';
|
import { match } from 'path-to-regexp';
|
||||||
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||||
|
|
||||||
const path = '/actor/:actorId/:actorSlug?/:domain(scenes|movies)?/:scope?/:page?';
|
const path = '/actor/:actorId(\\d+)/:actorSlug?/:domain(scenes|movies)?/:scope?/:page?';
|
||||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||||
|
|
||||||
export default (pageContext) => {
|
export default (pageContext) => {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
||||||
|
export default '/actor/edit/@actorId/*';
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div class="content">
|
||||||
|
<div class="revs-header">
|
||||||
|
<h2 class="heading">Revisions for "{{ scene.title }}"</h2>
|
||||||
|
|
||||||
|
<div class="revs-actions">
|
||||||
|
<a
|
||||||
|
:href="`/scene/edit/${scene.id}/${scene.slug}`"
|
||||||
|
class="link"
|
||||||
|
>Edit scene</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>Go to scene</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Revisions context="scene" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { inject } from 'vue';
|
||||||
|
|
||||||
|
import Revisions from '#/components/edit/revisions.vue';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const scene = pageContext.pageProps.scene;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.content {
|
||||||
|
padding: 1rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.revs-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(--compact) {
|
||||||
|
.revs-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { fetchActorsById } from '#/src/actors.js';
|
||||||
|
import { fetchSceneRevisions } from '#/src/scenes.js';
|
||||||
|
|
||||||
|
export async function onBeforeRender(pageContext) {
|
||||||
|
const [actor] = await fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user);
|
||||||
|
|
||||||
|
const {
|
||||||
|
revisions,
|
||||||
|
} = await fetchSceneRevisions(null, {
|
||||||
|
sceneId: actor.id,
|
||||||
|
isFinalized: true,
|
||||||
|
limit: 100,
|
||||||
|
}, pageContext.user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContext: {
|
||||||
|
title: `Revisions for '${actor.name}'`,
|
||||||
|
pageProps: {
|
||||||
|
actor,
|
||||||
|
revisions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export default '/actor/revisions/@actorId/*';
|
|
@ -1 +0,0 @@
|
||||||
export default '/admin/@section/*';
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||||
|
import { fetchActorRevisions } from '#/src/actors.js';
|
||||||
|
|
||||||
|
export async function onBeforeRender(pageContext) {
|
||||||
|
if (!pageContext.user || pageContext.user.role === 'user') {
|
||||||
|
throw render(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
revisions,
|
||||||
|
} = await fetchActorRevisions(null, {
|
||||||
|
isFinalized: false,
|
||||||
|
limit: 50,
|
||||||
|
}, pageContext.user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContext: {
|
||||||
|
title: pageContext.routeParams.section,
|
||||||
|
pageProps: {
|
||||||
|
revisions,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { match } from 'path-to-regexp';
|
||||||
|
|
||||||
|
const path = '/admin/:section/:domain(actors)';
|
||||||
|
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||||
|
|
||||||
|
export default (pageContext) => {
|
||||||
|
const matched = urlMatch(pageContext.urlPathname);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
return {
|
||||||
|
routeParams: {
|
||||||
|
section: matched.params.section,
|
||||||
|
domain: matched.params.domain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { match } from 'path-to-regexp';
|
||||||
|
|
||||||
|
const path = '/admin/:section/:domain(scenes)';
|
||||||
|
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||||
|
|
||||||
|
export default (pageContext) => {
|
||||||
|
const matched = urlMatch(pageContext.urlPathname);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
return {
|
||||||
|
routeParams: {
|
||||||
|
section: matched.params.section,
|
||||||
|
domain: matched.params.domain,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -36,8 +36,11 @@
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="network.hasLogo"
|
v-if="network.hasLogo"
|
||||||
:src="network.type === 'network' || network.isIndependent || !network.parent ? `/logos/${network.slug}/thumbs/network.png` : `/logos/${network.parent.slug}/thumbs/${network.slug}.png`"
|
:src="network.type === 'network' || network.isIndependent || !network.parent
|
||||||
|
? `/logos/${network.slug}/thumbs/network.png`
|
||||||
|
: `/logos/${network.parent.slug}/thumbs/${network.slug}.png`"
|
||||||
:alt="network.name"
|
:alt="network.name"
|
||||||
|
loading="lazy"
|
||||||
class="logo"
|
class="logo"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
@ -58,7 +61,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, inject } from 'vue';
|
import { ref, inject, onMounted } from 'vue';
|
||||||
|
|
||||||
import navigate from '#/src/navigate.js';
|
import navigate from '#/src/navigate.js';
|
||||||
|
|
||||||
|
@ -120,6 +123,12 @@ const sections = [
|
||||||
async function search() {
|
async function search() {
|
||||||
navigate('/channels', { q: query.value || undefined }, { redirect: true });
|
navigate('/channels', { q: query.value || undefined }, { redirect: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('load', (event) => {
|
||||||
|
console.log(event);
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -194,6 +203,9 @@ async function search() {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(--small-30) {
|
@media(--small-30) {
|
||||||
|
|
|
@ -676,7 +676,9 @@ function copySummary() {
|
||||||
|
|
||||||
.templates {
|
.templates {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: .25rem 0;
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
<li v-if="user.role !== 'user'">
|
<li v-if="user.role !== 'user'">
|
||||||
<a
|
<a
|
||||||
href="/admin/revisions"
|
href="/admin/revisions/scenes"
|
||||||
class="link"
|
class="link"
|
||||||
>Go to revisions admin</a>
|
>Go to revisions admin</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
@submit.prevent
|
@submit.prevent
|
||||||
>
|
>
|
||||||
<div class="editor-header">
|
<div class="editor-header">
|
||||||
<h2 class="heading ellipsis">Edit scene #{{ scene.id }}</h2>
|
<h2 class="heading ellipsis">Edit scene #{{ scene.id }} - {{ scene.title }}</h2>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="`/scene/${scene.id}/${scene.slug}`"
|
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||||
|
|
382
src/actors.js
382
src/actors.js
|
@ -1,7 +1,11 @@
|
||||||
import config from 'config';
|
import config from 'config';
|
||||||
import { differenceInYears } from 'date-fns';
|
import { differenceInYears } from 'date-fns';
|
||||||
import { unit } from 'mathjs';
|
import { unit } from 'mathjs';
|
||||||
|
import { MerkleJson } from 'merkle-json';
|
||||||
|
import moment from 'moment';
|
||||||
|
import omit from 'object.omit';
|
||||||
|
|
||||||
|
import initLogger from './logger.js';
|
||||||
import { knexOwner as knex, knexManticore } from './knex.js';
|
import { knexOwner as knex, knexManticore } from './knex.js';
|
||||||
import { utilsApi } from './manticore.js';
|
import { utilsApi } from './manticore.js';
|
||||||
import { HttpError } from './errors.js';
|
import { HttpError } from './errors.js';
|
||||||
|
@ -11,6 +15,11 @@ import { curateMedia } from './media.js';
|
||||||
import { curateStash } from './stashes.js';
|
import { curateStash } from './stashes.js';
|
||||||
import escape from '../utils/escape-manticore.js';
|
import escape from '../utils/escape-manticore.js';
|
||||||
import slugify from '../utils/slugify.js';
|
import slugify from '../utils/slugify.js';
|
||||||
|
import { curateRevision } from './revisions.js';
|
||||||
|
import { interpolateProfiles } from '../common/actors.mjs'; // eslint-disable-line import/namespace
|
||||||
|
|
||||||
|
const logger = initLogger();
|
||||||
|
const mj = new MerkleJson();
|
||||||
|
|
||||||
export function curateActor(actor, context = {}) {
|
export function curateActor(actor, context = {}) {
|
||||||
return {
|
return {
|
||||||
|
@ -22,11 +31,22 @@ export function curateActor(actor, context = {}) {
|
||||||
dateOfBirth: actor.date_of_birth,
|
dateOfBirth: actor.date_of_birth,
|
||||||
ageFromBirth: actor.date_of_birth && differenceInYears(Date.now(), actor.date_of_birth),
|
ageFromBirth: actor.date_of_birth && differenceInYears(Date.now(), actor.date_of_birth),
|
||||||
ageThen: context.sceneDate && actor.date_of_birth && differenceInYears(context.sceneDate, actor.date_of_birth),
|
ageThen: context.sceneDate && actor.date_of_birth && differenceInYears(context.sceneDate, actor.date_of_birth),
|
||||||
|
dateOfDeath: actor.date_of_death,
|
||||||
bust: actor.bust,
|
bust: actor.bust,
|
||||||
cup: actor.cup,
|
cup: actor.cup,
|
||||||
waist: actor.waist,
|
waist: actor.waist,
|
||||||
hip: actor.hip,
|
hip: actor.hip,
|
||||||
naturalBoobs: actor.natural_boobs,
|
naturalBoobs: actor.natural_boobs,
|
||||||
|
boobsVolume: actor.boobs_volume,
|
||||||
|
boobsImplant: actor.boobs_implant,
|
||||||
|
boobsPlacement: actor.boobs_placement,
|
||||||
|
boobsSurgeon: actor.boobs_surgeon,
|
||||||
|
naturalButt: actor.natural_butt,
|
||||||
|
buttVolume: actor.butt_volume,
|
||||||
|
buttImplant: actor.butt_implant,
|
||||||
|
penisLength: actor.penis_length,
|
||||||
|
penisGirth: actor.penis_girth,
|
||||||
|
isCircumcised: actor.is_circumcised,
|
||||||
height: actor.height && {
|
height: actor.height && {
|
||||||
metric: actor.height,
|
metric: actor.height,
|
||||||
imperial: unit(actor.height, 'cm').splitUnit(['ft', 'in']).map((value) => Math.round(value.toNumber())),
|
imperial: unit(actor.height, 'cm').splitUnit(['ft', 'in']).map((value) => Math.round(value.toNumber())),
|
||||||
|
@ -139,6 +159,8 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||||
: [],
|
: [],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
console.log(actors);
|
||||||
|
|
||||||
if (options.order) {
|
if (options.order) {
|
||||||
return actors.map((actorEntry) => curateActor(actorEntry, {
|
return actors.map((actorEntry) => curateActor(actorEntry, {
|
||||||
stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
|
stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
|
||||||
|
@ -161,6 +183,8 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||||
});
|
});
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
console.log(curatedActors);
|
||||||
|
|
||||||
return curatedActors;
|
return curatedActors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,3 +401,361 @@ export async function fetchActors(filters, rawOptions, reqUser) {
|
||||||
limit: options.limit,
|
limit: options.limit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchActorRevisions(revisionId, filters = {}, reqUser) {
|
||||||
|
const limit = filters.limit || 50;
|
||||||
|
const page = filters.page || 1;
|
||||||
|
|
||||||
|
const revisions = await knex('actors_revisions')
|
||||||
|
.select(
|
||||||
|
'actors_revisions.*',
|
||||||
|
'users.username as username',
|
||||||
|
'reviewers.username as reviewer_username',
|
||||||
|
)
|
||||||
|
.leftJoin('users', 'users.id', 'actors_revisions.user_id')
|
||||||
|
.leftJoin('users as reviewers', 'reviewers.id', 'actors_revisions.reviewed_by')
|
||||||
|
.modify((builder) => {
|
||||||
|
if (!['admin', 'editor'].includes(reqUser?.role) && !filters.userId && !filters.actorId) {
|
||||||
|
builder.where('user_id', reqUser.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.userId) {
|
||||||
|
if (!['admin', 'editor'].includes(reqUser?.role) && filters.userId !== reqUser.id) {
|
||||||
|
throw new HttpError('You are not permitted to view revisions from other users.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.where('actors_revisions.user_id', filters.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (revisionId) {
|
||||||
|
builder.where('actors_revisions.id', revisionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.actorId) {
|
||||||
|
builder.where('actors_revisions.actor_id', filters.actorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.isFinalized === false) {
|
||||||
|
builder.whereNull('approved');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.isFinalized === true) {
|
||||||
|
builder.whereNotNull('approved');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.orderBy('created_at', 'desc')
|
||||||
|
.limit(limit)
|
||||||
|
.offset((page - 1) * limit);
|
||||||
|
|
||||||
|
const curatedRevisions = revisions.map((revision) => curateRevision(revision));
|
||||||
|
|
||||||
|
return {
|
||||||
|
revisions: curatedRevisions,
|
||||||
|
revision: revisionId && curatedRevisions.find((revision) => revision.id === revisionId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyMap = {
|
||||||
|
avatar: 'avatar_media_id',
|
||||||
|
dateOfBirth: 'date_of_birth',
|
||||||
|
dateOfDeath: 'date_of_death',
|
||||||
|
originCountry: 'birth_country_alpha2',
|
||||||
|
originState: 'birth_state',
|
||||||
|
originCity: 'birth_city',
|
||||||
|
residenceCountry: 'residence_country_alpha2',
|
||||||
|
residenceState: 'residence_state',
|
||||||
|
residenceCity: 'residence_city',
|
||||||
|
hairColor: 'hair_color',
|
||||||
|
naturalBoobs: 'natural_boobs',
|
||||||
|
boobsVolume: 'boobs_volume',
|
||||||
|
boobsImplant: 'boobs_implant',
|
||||||
|
boobsPlacement: 'boobs_placement',
|
||||||
|
boobsSurgeon: 'boobs_surgeon',
|
||||||
|
naturalButt: 'natural_butt',
|
||||||
|
buttVolume: 'butt_volume',
|
||||||
|
buttImplant: 'butt_implant',
|
||||||
|
penisLength: 'penis_length',
|
||||||
|
penisGirth: 'penis_girth',
|
||||||
|
isCircumcised: 'circumcised',
|
||||||
|
hasTattoos: 'has_tattoos',
|
||||||
|
hasPiercings: 'has_piercings',
|
||||||
|
};
|
||||||
|
|
||||||
|
async function applyActorValueDelta(profileId, delta, trx) {
|
||||||
|
console.log('value delta', profileId, delta, keyMap[delta.key], delta.value);
|
||||||
|
|
||||||
|
return knex('actors_profiles')
|
||||||
|
.where('id', profileId)
|
||||||
|
.update(keyMap[delta.key] || delta.key, delta.value)
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyActorDirectDelta(actorId, delta, trx) {
|
||||||
|
console.log('value delta', delta);
|
||||||
|
|
||||||
|
return knex('actors')
|
||||||
|
.where('id', actorId)
|
||||||
|
.update(keyMap[delta.key] || delta.key, delta.value)
|
||||||
|
.modify((builder) => {
|
||||||
|
if (delta.key === 'name') {
|
||||||
|
builder.update('slug', slugify(delta.value));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMainProfile(actorId, wasCreated = false) {
|
||||||
|
const profileEntry = await knex('actors_profiles')
|
||||||
|
.where('actor_id', actorId)
|
||||||
|
.where('entity_id', null)
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (profileEntry) {
|
||||||
|
return profileEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasCreated) {
|
||||||
|
throw new HttpError('Failed to find or create main profile', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex('actors_profiles').insert({
|
||||||
|
actor_id: actorId,
|
||||||
|
entity_id: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetchMainProfile(actorId, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
async function applyMainProfile(actorId) {
|
||||||
|
const [actorEntry, mainProfile] = await Promise.all([
|
||||||
|
knex('actors')
|
||||||
|
.where('id', actorId)
|
||||||
|
.first(),
|
||||||
|
fetchMainProfile(actorId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!actorEntry) {
|
||||||
|
throw new HttpError('No actor profile found to apply main profile to', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const preservedKeys = ['id'];
|
||||||
|
|
||||||
|
// we start iterating from the actor entry so we don't include keys that are not yet supported by the actors table
|
||||||
|
const mergedProfile = Object.fromEntries(Object.entries(actorEntry)
|
||||||
|
.filter(([key]) => Object.hasOwn(mainProfile, key))
|
||||||
|
.map(([key, value]) => [key, mainProfile[key] === null || preservedKeys.includes(key)
|
||||||
|
? value
|
||||||
|
: mainProfile[key]]));
|
||||||
|
|
||||||
|
await knex('actors')
|
||||||
|
.where('id', actorId)
|
||||||
|
.update(mergedProfile);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function applyActorRevision(revisionIds, reqUser) {
|
||||||
|
const revisions = await knex('actors_revisions')
|
||||||
|
.whereIn('id', revisionIds)
|
||||||
|
.whereNull('applied_at'); // should not re-apply revision that was already applied
|
||||||
|
|
||||||
|
await revisions.reduce(async (chain, revision) => {
|
||||||
|
await chain;
|
||||||
|
|
||||||
|
const mainProfile = await fetchMainProfile(revision.actor_id);
|
||||||
|
|
||||||
|
await knex.transaction(async (trx) => {
|
||||||
|
await Promise.all(revision.deltas.map(async (delta) => {
|
||||||
|
if ([
|
||||||
|
'gender',
|
||||||
|
'avatar',
|
||||||
|
'dateOfBirth',
|
||||||
|
'dateOfDeath',
|
||||||
|
'originCountry',
|
||||||
|
'originState',
|
||||||
|
'originCity',
|
||||||
|
'residenceCountry',
|
||||||
|
'residenceState',
|
||||||
|
'residenceCity',
|
||||||
|
'height',
|
||||||
|
'weight',
|
||||||
|
'bust',
|
||||||
|
'cup',
|
||||||
|
'waist',
|
||||||
|
'hip',
|
||||||
|
'naturalBoobs',
|
||||||
|
'boobsVolume',
|
||||||
|
'boobsImplant',
|
||||||
|
'boobsPlacement',
|
||||||
|
'boobsSurgeon',
|
||||||
|
'naturalButt',
|
||||||
|
'buttVolume',
|
||||||
|
'buttImplant',
|
||||||
|
'penisLength',
|
||||||
|
'penisGirth',
|
||||||
|
'isCircumcised',
|
||||||
|
'hairColor',
|
||||||
|
'eyes',
|
||||||
|
'hasTattoos',
|
||||||
|
'tattoos',
|
||||||
|
'hasPiercings',
|
||||||
|
'piercings',
|
||||||
|
].includes(delta.key)) {
|
||||||
|
return applyActorValueDelta(mainProfile.id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'name' && reqUser.role === 'admin') {
|
||||||
|
return applyActorDirectDelta(revision.actor_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
|
||||||
|
await knex('actors_revisions')
|
||||||
|
.transacting(trx)
|
||||||
|
.where('id', revision.id)
|
||||||
|
.update('applied_at', knex.fn.now());
|
||||||
|
|
||||||
|
// await trx.commit();
|
||||||
|
}).catch(async (error) => {
|
||||||
|
logger.error(`Failed to apply revision ${revision.id} on actor ${revision.actor_id}: ${error.message}`);
|
||||||
|
});
|
||||||
|
}, Promise.resolve());
|
||||||
|
|
||||||
|
const actorIds = Array.from(new Set(revisions.map((revision) => revision.actor_id)));
|
||||||
|
|
||||||
|
await interpolateProfiles(actorIds, {
|
||||||
|
knex,
|
||||||
|
logger,
|
||||||
|
moment,
|
||||||
|
slugify,
|
||||||
|
omit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reviewActorRevision(revisionId, isApproved, { feedback }, reqUser) {
|
||||||
|
if (!reqUser || reqUser.role === 'user') {
|
||||||
|
throw new HttpError('You are not permitted to approve revisions', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof isApproved !== 'boolean') {
|
||||||
|
throw new HttpError('You must either approve or reject the revision', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await knex('actors_revisions')
|
||||||
|
.where('id', revisionId)
|
||||||
|
.whereNull('approved') // don't rerun reviewed revision, must be forked into new revision instead
|
||||||
|
.whereNull('applied_at')
|
||||||
|
.update({
|
||||||
|
approved: isApproved,
|
||||||
|
reviewed_at: knex.fn.now(),
|
||||||
|
reviewed_by: reqUser.id,
|
||||||
|
feedback,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (updated === 0) {
|
||||||
|
throw new HttpError('This revision was already reviewed', 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isApproved) {
|
||||||
|
await applyActorRevision([revisionId], reqUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createActorRevision(actorId, { edits, comment, apply }, reqUser) {
|
||||||
|
const [
|
||||||
|
[actor],
|
||||||
|
openRevisions,
|
||||||
|
] = await Promise.all([
|
||||||
|
fetchActorsById([actorId], {
|
||||||
|
reqUser,
|
||||||
|
includeAssets: true,
|
||||||
|
includePartOf: true,
|
||||||
|
}),
|
||||||
|
knex('actors_revisions')
|
||||||
|
.where('user_id', reqUser.id)
|
||||||
|
.whereNull('approved'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!actor) {
|
||||||
|
throw new HttpError(`No actor with ID ${actorId} found to update`, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openRevisions.length >= config.revisions.unapprovedLimit) {
|
||||||
|
throw new HttpError(`You have ${config.revisions.unapprovedLimit} unapproved revisions, please wait for approval before submitting another revision.`, 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseActor = Object.fromEntries(Object.entries(actor).map(([key, values]) => {
|
||||||
|
if ([
|
||||||
|
'scenes',
|
||||||
|
'likes',
|
||||||
|
'stashes',
|
||||||
|
'profiles',
|
||||||
|
].includes(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* avatar should return id
|
||||||
|
if (values?.hash) {
|
||||||
|
return [key, values.hash];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (values?.id) {
|
||||||
|
return [key, values.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.metric) {
|
||||||
|
return [key, values.metric];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
return [key, values.map((value) => value?.hash || value?.id || value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [key, values];
|
||||||
|
}).filter(Boolean));
|
||||||
|
|
||||||
|
const deltas = Object.entries(edits).map(([key, value]) => {
|
||||||
|
if (baseActor[key] === value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const valueSet = new Set(value);
|
||||||
|
const baseSet = new Set(baseActor[key]);
|
||||||
|
|
||||||
|
if (valueSet.size === baseSet.size && baseActor[key].every((id) => valueSet.has(id))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value: Array.from(valueSet) };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value: value || null };
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
if (deltas.length === 0) {
|
||||||
|
throw new HttpError('No effective changes provided', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [revisionEntry] = await knex('actors_revisions')
|
||||||
|
.insert({
|
||||||
|
user_id: reqUser.id,
|
||||||
|
actor_id: actor.id,
|
||||||
|
base: JSON.stringify(baseActor),
|
||||||
|
deltas: JSON.stringify(deltas),
|
||||||
|
hash: mj.hash({
|
||||||
|
base: baseActor,
|
||||||
|
deltas,
|
||||||
|
}),
|
||||||
|
comment,
|
||||||
|
})
|
||||||
|
.returning('id');
|
||||||
|
|
||||||
|
if (['admin', 'editor'].includes(reqUser.role) && apply) {
|
||||||
|
// don't keep the editor waiting for the revision to apply
|
||||||
|
reviewActorRevision(revisionEntry.id, true, {}, reqUser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -28,3 +28,10 @@ export async function fetchCountriesByAlpha2(alpha2s, options = {}) {
|
||||||
|
|
||||||
return entries.map((countryEntry) => curateCountry(countryEntry));
|
return entries.map((countryEntry) => curateCountry(countryEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchCountries() {
|
||||||
|
const entries = await knex('countries')
|
||||||
|
.orderBy(knex.raw('coalesce(alias, name)'));
|
||||||
|
|
||||||
|
return entries.map((countryEntry) => curateCountry(countryEntry));
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export function curateMedia(media, context = {}) {
|
||||||
width: media.width,
|
width: media.width,
|
||||||
height: media.height,
|
height: media.height,
|
||||||
index: media.index,
|
index: media.index,
|
||||||
|
sharpness: media.sharpness,
|
||||||
credit: media.credit,
|
credit: media.credit,
|
||||||
type: context.type || null,
|
type: context.type || null,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
export function curateRevision(revision) {
|
||||||
|
return {
|
||||||
|
id: revision.id,
|
||||||
|
sceneId: revision.scene_id,
|
||||||
|
actorId: revision.actor_id,
|
||||||
|
base: revision.base,
|
||||||
|
deltas: revision.deltas,
|
||||||
|
hash: revision.hash,
|
||||||
|
comment: revision.comment,
|
||||||
|
user: revision.user_id && {
|
||||||
|
id: revision.user_id,
|
||||||
|
username: revision.username,
|
||||||
|
},
|
||||||
|
review: typeof revision.approved === 'boolean' ? {
|
||||||
|
isApproved: revision.approved,
|
||||||
|
userId: revision.reviewed_by,
|
||||||
|
username: revision.reviewer_username,
|
||||||
|
reviewedAt: revision.reviewed_at,
|
||||||
|
} : null,
|
||||||
|
appliedAt: revision.applied_at,
|
||||||
|
failed: revision.failed,
|
||||||
|
createdAt: revision.created_at,
|
||||||
|
};
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { curateMedia } from './media.js';
|
||||||
import escape from '../utils/escape-manticore.js';
|
import escape from '../utils/escape-manticore.js';
|
||||||
import promiseProps from '../utils/promise-props.js';
|
import promiseProps from '../utils/promise-props.js';
|
||||||
import initLogger from './logger.js';
|
import initLogger from './logger.js';
|
||||||
|
import { curateRevision } from './revisions.js';
|
||||||
|
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
const mj = new MerkleJson();
|
const mj = new MerkleJson();
|
||||||
|
@ -58,8 +59,6 @@ function curateScene(rawScene, assets) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(assets.chapters);
|
|
||||||
|
|
||||||
const curatedScene = {
|
const curatedScene = {
|
||||||
id: rawScene.id,
|
id: rawScene.id,
|
||||||
title: rawScene.title,
|
title: rawScene.title,
|
||||||
|
@ -634,30 +633,6 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function curateRevision(revision) {
|
|
||||||
return {
|
|
||||||
id: revision.id,
|
|
||||||
sceneId: revision.scene_id,
|
|
||||||
base: revision.base,
|
|
||||||
deltas: revision.deltas,
|
|
||||||
hash: revision.hash,
|
|
||||||
comment: revision.comment,
|
|
||||||
user: revision.user_id && {
|
|
||||||
id: revision.user_id,
|
|
||||||
username: revision.username,
|
|
||||||
},
|
|
||||||
review: typeof revision.approved === 'boolean' ? {
|
|
||||||
isApproved: revision.approved,
|
|
||||||
userId: revision.reviewed_by,
|
|
||||||
username: revision.reviewer_username,
|
|
||||||
reviewedAt: revision.reviewed_at,
|
|
||||||
} : null,
|
|
||||||
appliedAt: revision.applied_at,
|
|
||||||
failed: revision.failed,
|
|
||||||
createdAt: revision.created_at,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
|
export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
|
||||||
const limit = filters.limit || 50;
|
const limit = filters.limit || 50;
|
||||||
const page = filters.page || 1;
|
const page = filters.page || 1;
|
||||||
|
@ -727,6 +702,9 @@ export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
|
||||||
|
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
productionDate: 'production_date',
|
productionDate: 'production_date',
|
||||||
|
productionLocation: 'production_location',
|
||||||
|
productionCity: 'production_city',
|
||||||
|
productionState: 'production_state',
|
||||||
};
|
};
|
||||||
|
|
||||||
async function applySceneValueDelta(sceneId, delta, trx) {
|
async function applySceneValueDelta(sceneId, delta, trx) {
|
||||||
|
@ -952,6 +930,7 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
|
||||||
.returning('id');
|
.returning('id');
|
||||||
|
|
||||||
if (['admin', 'editor'].includes(reqUser.role) && apply) {
|
if (['admin', 'editor'].includes(reqUser.role) && apply) {
|
||||||
await reviewSceneRevision(revisionEntry.id, true, {}, reqUser);
|
// don't keep the editor waiting for the revision to apply
|
||||||
|
reviewSceneRevision(revisionEntry.id, true, {}, reqUser);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
import Router from 'express-promise-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchActors,
|
fetchActors,
|
||||||
fetchActorsById,
|
fetchActorsById,
|
||||||
|
fetchActorRevisions,
|
||||||
|
createActorRevision,
|
||||||
|
reviewActorRevision,
|
||||||
} from '../actors.js';
|
} from '../actors.js';
|
||||||
|
|
||||||
export function curateActorsQuery(query) {
|
export function curateActorsQuery(query) {
|
||||||
|
@ -131,11 +136,36 @@ export async function fetchActorsByIdGraphql(query, _req, _info) {
|
||||||
const actors = await fetchActorsById([].concat(query.id, query.ids).filter(Boolean));
|
const actors = await fetchActorsById([].concat(query.id, query.ids).filter(Boolean));
|
||||||
const curatedActors = actors.map((actor) => curateGraphqlActor(actor));
|
const curatedActors = actors.map((actor) => curateGraphqlActor(actor));
|
||||||
|
|
||||||
console.log(actors);
|
|
||||||
|
|
||||||
if (query.ids) {
|
if (query.ids) {
|
||||||
return curatedActors;
|
return curatedActors;
|
||||||
}
|
}
|
||||||
|
|
||||||
return curatedActors[0];
|
return curatedActors[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchActorRevisionsApi(req, res) {
|
||||||
|
const revisions = await fetchActorRevisions(Number(req.params.revisionId) || null, req.query, req.user);
|
||||||
|
|
||||||
|
res.send(revisions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createActorRevisionApi(req, res) {
|
||||||
|
await createActorRevision(Number(req.body.actorId), req.body, req.user);
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reviewActorRevisionApi(req, res) {
|
||||||
|
await reviewActorRevision(Number(req.params.revisionId), req.body.isApproved, req.body, req.user);
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actorsRouter = Router();
|
||||||
|
|
||||||
|
actorsRouter.get('/api/actors', fetchActorsApi);
|
||||||
|
|
||||||
|
actorsRouter.get('/api/revisions/actors', fetchActorRevisionsApi);
|
||||||
|
actorsRouter.get('/api/revisions/actors/:revisionId', fetchActorRevisionsApi);
|
||||||
|
actorsRouter.post('/api/revisions/actors', createActorRevisionApi);
|
||||||
|
actorsRouter.post('/api/revisions/actors/:revisionId/reviews', reviewActorRevisionApi);
|
||||||
|
|
|
@ -252,7 +252,7 @@ export const scenesRouter = Router();
|
||||||
scenesRouter.get('/api/scenes', fetchScenesApi);
|
scenesRouter.get('/api/scenes', fetchScenesApi);
|
||||||
scenesRouter.get('/api/scenes/:sceneId', fetchSceneApi);
|
scenesRouter.get('/api/scenes/:sceneId', fetchSceneApi);
|
||||||
|
|
||||||
scenesRouter.get('/api/revisions', fetchSceneRevisionsApi);
|
scenesRouter.get('/api/revisions/scenes', fetchSceneRevisionsApi);
|
||||||
scenesRouter.get('/api/revisions/:revisionId', fetchSceneRevisionsApi);
|
scenesRouter.get('/api/revisions/scenes/:revisionId', fetchSceneRevisionsApi);
|
||||||
scenesRouter.post('/api/revisions', createSceneRevisionApi);
|
scenesRouter.post('/api/revisions/scenes', createSceneRevisionApi);
|
||||||
scenesRouter.post('/api/revisions/:revisionId/reviews', reviewSceneRevisionApi);
|
scenesRouter.post('/api/revisions/scenes/:revisionId/reviews', reviewSceneRevisionApi);
|
||||||
|
|
|
@ -14,8 +14,8 @@ import errorHandler from './error.js';
|
||||||
import consentHandler from './consent.js';
|
import consentHandler from './consent.js';
|
||||||
|
|
||||||
import { scenesRouter } from './scenes.js';
|
import { scenesRouter } from './scenes.js';
|
||||||
|
import { actorsRouter } from './actors.js';
|
||||||
|
|
||||||
import { fetchActorsApi } from './actors.js';
|
|
||||||
import { fetchMoviesApi } from './movies.js';
|
import { fetchMoviesApi } from './movies.js';
|
||||||
import { fetchEntitiesApi } from './entities.js';
|
import { fetchEntitiesApi } from './entities.js';
|
||||||
import { fetchTagsApi } from './tags.js';
|
import { fetchTagsApi } from './tags.js';
|
||||||
|
@ -144,9 +144,7 @@ export default async function initServer() {
|
||||||
router.use(userRouter);
|
router.use(userRouter);
|
||||||
router.use(stashesRouter);
|
router.use(stashesRouter);
|
||||||
router.use(scenesRouter);
|
router.use(scenesRouter);
|
||||||
|
router.use(actorsRouter);
|
||||||
// ACTORS
|
|
||||||
router.get('/api/actors', fetchActorsApi);
|
|
||||||
|
|
||||||
// MOVIES
|
// MOVIES
|
||||||
router.get('/api/movies', fetchMoviesApi);
|
router.get('/api/movies', fetchMoviesApi);
|
||||||
|
|
2
static
2
static
|
@ -1 +1 @@
|
||||||
Subproject commit 0f7fc7c9f4ba7454bb92482eaffd93b345ea9834
|
Subproject commit 083dfa7120cedc41757fe0da08d073faa6898e9f
|
Loading…
Reference in New Issue