Added experimental edit page and revision history.
This commit is contained in:
parent
4b8dfba289
commit
8bf9e22b39
|
@ -57,3 +57,7 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noshrink {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<VDropdown
|
||||||
|
:disabled="disabled"
|
||||||
|
class="trigger"
|
||||||
|
@show="focus"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref="queryInput"
|
||||||
|
v-model="query"
|
||||||
|
placeholder="Search actor"
|
||||||
|
class="input"
|
||||||
|
@input="search"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ul class="actors nolist">
|
||||||
|
<li
|
||||||
|
v-for="actor in actors"
|
||||||
|
:key="`actor-${actor.slug}`"
|
||||||
|
v-close-popper
|
||||||
|
class="actor"
|
||||||
|
@click="emit('actor', actor)"
|
||||||
|
>{{ actor.name }} ({{ [actor.ageFromBirth, actor.origin?.country?.alpha2].join(', ') }})
|
||||||
|
<img
|
||||||
|
v-if="actor.avatar"
|
||||||
|
:src="getPath(actor.avatar, 'thumbnail')"
|
||||||
|
class="avatar"
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject } from 'vue';
|
||||||
|
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const actorNames = {
|
||||||
|
dp: 'double penetration',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultActors = pageContext.pageProps.actorIds
|
||||||
|
? Object.entries(pageContext.pageProps.actorIds).map(([slug, id]) => ({
|
||||||
|
id,
|
||||||
|
slug,
|
||||||
|
name: actorNames[slug] || slug,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const actors = ref(defaultActors);
|
||||||
|
const query = ref(null);
|
||||||
|
const queryInput = ref(null);
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['actor']);
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
const data = await get('/actors', { q: query.value });
|
||||||
|
|
||||||
|
actors.value = data.actors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryInput.value?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trigger {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor {
|
||||||
|
display: block;
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--glass-weak-50);
|
||||||
|
color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
position: fixed;
|
||||||
|
display: none;
|
||||||
|
left: 7rem;
|
||||||
|
width: 8rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,153 @@
|
||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
class="actors nolist"
|
||||||
|
:class="{ disabled: !editing.has(item.key) }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="actor in [...item.value, ...newActors]"
|
||||||
|
:key="`actor-${actor.id}`"
|
||||||
|
class="actor"
|
||||||
|
:class="{ deleted: edits.actors && !edits.actors.some((actorId) => actorId === actor.id) }"
|
||||||
|
>
|
||||||
|
<span class="actor-name">{{ actor.name }}</span>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="edits.actors && !edits.actors.some((actorId) => actorId === actor.id)"
|
||||||
|
icon="checkmark"
|
||||||
|
class="add"
|
||||||
|
@click="emit('actors', edits.actors.concat(actor.id))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-else
|
||||||
|
icon="cross2"
|
||||||
|
class="remove"
|
||||||
|
@click="emit('actors', edits.actors.filter((actorId) => actorId !== actor.id))"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="new">
|
||||||
|
<ActorSearch
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@actor="addActor"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="plus3"
|
||||||
|
class="add"
|
||||||
|
/>
|
||||||
|
</ActorSearch>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import ActorSearch from '#/components/actors/search.vue';
|
||||||
|
|
||||||
|
const newActors = ref([]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['actors']);
|
||||||
|
|
||||||
|
function addActor(actor) {
|
||||||
|
newActors.value = newActors.value.concat(actor);
|
||||||
|
|
||||||
|
emit('actors', props.edits.actors.concat(actor.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.scene, () => { newActors.value = []; });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.actors {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .25rem;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
.actor {
|
||||||
|
color: var(--shadow);
|
||||||
|
|
||||||
|
.remove,
|
||||||
|
.add {
|
||||||
|
background: var(--shadow-weak-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new .icon {
|
||||||
|
background: var(--shadow-weak-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: .25rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
fill: var(--text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
background: var(--glass-weak-30);
|
||||||
|
border-radius: .25rem;
|
||||||
|
|
||||||
|
&.deleted {
|
||||||
|
color: var(--glass);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor,
|
||||||
|
.new {
|
||||||
|
.remove,
|
||||||
|
.add {
|
||||||
|
height: auto;
|
||||||
|
padding: .25rem .3rem;
|
||||||
|
fill: var(--highlight-strong-10);
|
||||||
|
border-radius: .25rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--text-light);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
background: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.actor-name {
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,153 @@
|
||||||
|
<template>
|
||||||
|
<ul
|
||||||
|
class="tags nolist"
|
||||||
|
:class="{ disabled: !editing.has(item.key) }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="tag in [...item.value, ...newTags]"
|
||||||
|
:key="`tag-${tag.id}`"
|
||||||
|
class="tag"
|
||||||
|
:class="{ deleted: edits.tags && !edits.tags.some((tagId) => tagId === tag.id) }"
|
||||||
|
>
|
||||||
|
<span class="tag-name">{{ tag.name }}</span>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="edits.tags && !edits.tags.some((tagId) => tagId === tag.id)"
|
||||||
|
icon="checkmark"
|
||||||
|
class="add"
|
||||||
|
@click="emit('tags', edits.tags.concat(tag.id))"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-else
|
||||||
|
icon="cross2"
|
||||||
|
class="remove"
|
||||||
|
@click="emit('tags', edits.tags.filter((tagId) => tagId !== tag.id))"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="new">
|
||||||
|
<TagSearch
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@tag="addTag"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="plus3"
|
||||||
|
class="add"
|
||||||
|
/>
|
||||||
|
</TagSearch>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import TagSearch from '#/components/tags/search.vue';
|
||||||
|
|
||||||
|
const newTags = ref([]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
edits: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
editing: {
|
||||||
|
type: Set,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['tags']);
|
||||||
|
|
||||||
|
function addTag(tag) {
|
||||||
|
newTags.value = newTags.value.concat(tag);
|
||||||
|
|
||||||
|
emit('tags', props.edits.tags.concat(tag.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.scene, () => { newTags.value = []; });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .25rem;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
.tag {
|
||||||
|
color: var(--shadow);
|
||||||
|
|
||||||
|
.remove,
|
||||||
|
.add {
|
||||||
|
background: var(--shadow-weak-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new .icon {
|
||||||
|
background: var(--shadow-weak-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: .25rem;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
fill: var(--text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
background: var(--glass-weak-30);
|
||||||
|
border-radius: .25rem;
|
||||||
|
|
||||||
|
&.deleted {
|
||||||
|
color: var(--glass);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag,
|
||||||
|
.new {
|
||||||
|
.remove,
|
||||||
|
.add {
|
||||||
|
height: auto;
|
||||||
|
padding: .25rem .3rem;
|
||||||
|
fill: var(--highlight-strong-10);
|
||||||
|
border-radius: .25rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
fill: var(--text-light);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
background: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
background: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-name {
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<VDropdown
|
||||||
|
:disabled="disabled"
|
||||||
|
class="trigger"
|
||||||
|
@show="focus"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
ref="queryInput"
|
||||||
|
v-model="query"
|
||||||
|
placeholder="Search tag"
|
||||||
|
class="input"
|
||||||
|
@input="search"
|
||||||
|
>
|
||||||
|
|
||||||
|
<ul class="tags nolist">
|
||||||
|
<li
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="`tag-${tag.slug}`"
|
||||||
|
v-close-popper
|
||||||
|
class="tag"
|
||||||
|
@click="emit('tag', tag)"
|
||||||
|
>{{ tag.name }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, inject } from 'vue';
|
||||||
|
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const tagNames = {
|
||||||
|
dp: 'double penetration',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTags = pageContext.pageProps.tagIds
|
||||||
|
? Object.entries(pageContext.pageProps.tagIds).map(([slug, id]) => ({
|
||||||
|
id,
|
||||||
|
slug,
|
||||||
|
name: tagNames[slug] || slug,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tags = ref(defaultTags);
|
||||||
|
const query = ref(null);
|
||||||
|
const queryInput = ref(null);
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['tag']);
|
||||||
|
|
||||||
|
async function search() {
|
||||||
|
tags.value = await get('/tags', { query: query.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryInput.value?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trigger {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: block;
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--glass-weak-50);
|
||||||
|
color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -69,6 +69,9 @@ module.exports = {
|
||||||
keyLimit: 5, // max keys per user
|
keyLimit: 5, // max keys per user
|
||||||
keyCooldown: 1, // minutes between key generation
|
keyCooldown: 1, // minutes between key generation
|
||||||
},
|
},
|
||||||
|
revisions: {
|
||||||
|
unapprovedLimit: 3,
|
||||||
|
},
|
||||||
psa: {
|
psa: {
|
||||||
text: 'Welcome to traxxx!', // html enabled
|
text: 'Welcome to traxxx!', // html enabled
|
||||||
type: 'notice', // notice, alert
|
type: 'notice', // notice, alert
|
||||||
|
|
|
@ -196,6 +196,16 @@
|
||||||
{{ scene.shootId }}
|
{{ scene.shootId }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<time
|
||||||
|
v-if="scene.productionDate"
|
||||||
|
:datetime="formatDate(scene.productionDate, 'yyyy-MM-dd')"
|
||||||
|
:title="formatDate(scene.productionDate, 'yyyy-MM-dd')"
|
||||||
|
class="detail"
|
||||||
|
>
|
||||||
|
<h3 class="heading">Shoot date</h3>
|
||||||
|
{{ formatDate(scene.productionDate, 'MMMM d, yyyy') }}
|
||||||
|
</time>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="scene.studio"
|
v-if="scene.studio"
|
||||||
class="detail"
|
class="detail"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
import { render, redirect } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||||
import { fetchScenesById } from '#/src/scenes.js';
|
import { fetchScenesById } from '#/src/scenes.js';
|
||||||
import { getRandomCampaigns } from '#/src/campaigns.js';
|
import { getRandomCampaigns } from '#/src/campaigns.js';
|
||||||
|
import { getIdsBySlug } from '#/src/cache.js';
|
||||||
|
|
||||||
function getTitle(scene) {
|
function getTitle(scene) {
|
||||||
if (scene.title) {
|
if (scene.title) {
|
||||||
|
@ -15,6 +16,10 @@ function getTitle(scene) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
|
if (pageContext._pageId === '/pages/scene/edit' && !pageContext.user) {
|
||||||
|
throw redirect(`/login?r=${encodeURIComponent(pageContext.urlOriginal)}`);
|
||||||
|
}
|
||||||
|
|
||||||
const [scene] = await fetchScenesById([Number(pageContext.routeParams.sceneId)], {
|
const [scene] = await fetchScenesById([Number(pageContext.routeParams.sceneId)], {
|
||||||
reqUser: pageContext.user,
|
reqUser: pageContext.user,
|
||||||
includeAssets: true,
|
includeAssets: true,
|
||||||
|
@ -22,13 +27,21 @@ export async function onBeforeRender(pageContext) {
|
||||||
actorStashes: true,
|
actorStashes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const campaigns = await getRandomCampaigns([
|
const [campaigns, tagIds] = await Promise.all([
|
||||||
|
getRandomCampaigns([
|
||||||
{
|
{
|
||||||
minRatio: 1.5,
|
minRatio: 1.5,
|
||||||
entityIds: [scene.channel.id, scene.network?.id].filter(Boolean),
|
entityIds: [scene.channel.id, scene.network?.id].filter(Boolean),
|
||||||
allowRandomFallback: false,
|
allowRandomFallback: false,
|
||||||
},
|
},
|
||||||
], { tagFilter: pageContext.tagFilter });
|
], { tagFilter: pageContext.tagFilter }),
|
||||||
|
getIdsBySlug([
|
||||||
|
'anal',
|
||||||
|
'creampie',
|
||||||
|
'dp',
|
||||||
|
'facial',
|
||||||
|
], 'tags', true),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
throw render(404, `Cannot find scene '${pageContext.routeParams.sceneId}'.`);
|
throw render(404, `Cannot find scene '${pageContext.routeParams.sceneId}'.`);
|
||||||
|
@ -39,6 +52,7 @@ export async function onBeforeRender(pageContext) {
|
||||||
title: getTitle(scene),
|
title: getTitle(scene),
|
||||||
pageProps: {
|
pageProps: {
|
||||||
scene,
|
scene,
|
||||||
|
tagIds,
|
||||||
},
|
},
|
||||||
campaigns: {
|
campaigns: {
|
||||||
scene: campaigns[0],
|
scene: campaigns[0],
|
||||||
|
|
|
@ -0,0 +1,395 @@
|
||||||
|
<template>
|
||||||
|
<div class="editor">
|
||||||
|
<form @submit.prevent>
|
||||||
|
<div class="editor-header">
|
||||||
|
<h2 class="heading ellipsis">Edit scene #{{ scene.id }}</h2>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||||
|
target="_blank"
|
||||||
|
class="link noshrink"
|
||||||
|
>Go to scene</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="nolist">
|
||||||
|
<li
|
||||||
|
v-for="item in fields"
|
||||||
|
:key="`item-${item.key}`"
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div class="item-header">
|
||||||
|
<div class="key">{{ item.label || item.key }}</div>
|
||||||
|
|
||||||
|
<div class="item-actions">
|
||||||
|
<Icon
|
||||||
|
v-if="!item.forced"
|
||||||
|
icon="pencil5"
|
||||||
|
:class="{ active: editing.has(item.key) }"
|
||||||
|
@click="toggleField(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="value"
|
||||||
|
:class="{ disabled: !editing.has(item.key) }"
|
||||||
|
>
|
||||||
|
<EditActors
|
||||||
|
v-if="item.type === 'actors'"
|
||||||
|
:scene="scene"
|
||||||
|
:item="item"
|
||||||
|
:edits="edits"
|
||||||
|
:editing="editing"
|
||||||
|
@actors="(actors) => { edits.actors = actors; }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditTags
|
||||||
|
v-if="item.type === 'tags'"
|
||||||
|
:scene="scene"
|
||||||
|
:item="item"
|
||||||
|
:edits="edits"
|
||||||
|
:editing="editing"
|
||||||
|
@tags="(tags) => { edits.tags = tags; }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-if="item.type === 'string'"
|
||||||
|
:value="edits[item.key] || item.value"
|
||||||
|
class="string input"
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@input="setValue(item, $event)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
v-if="item.type === 'text'"
|
||||||
|
:value="edits[item.key] || item.value"
|
||||||
|
:placeholder="item.placeholder"
|
||||||
|
rows="3"
|
||||||
|
class="text input"
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@input="setValue(item, $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-if="item.type === 'date'"
|
||||||
|
type="datetime-local"
|
||||||
|
:value="edits[item.key] || item.value"
|
||||||
|
class="date input"
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@input="setValue(item, $event)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="item.type === 'duration'"
|
||||||
|
class="duration"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
:value="item.value[0]"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@input="setDuration('h', $event)"
|
||||||
|
>H
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
:value="item.value[1]"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@input="setDuration('m', $event)"
|
||||||
|
>M
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
:value="item.value[2]"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
:disabled="!editing.has(item.key)"
|
||||||
|
@input="setDuration('s', $event)"
|
||||||
|
>S
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="editor-footer">
|
||||||
|
<div class="comment">
|
||||||
|
<textarea
|
||||||
|
v-model="comment"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Please provide verifiable information supporting your edits."
|
||||||
|
class="text input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-actions">
|
||||||
|
<!-- we don't want the return key to submit the form -->
|
||||||
|
<button
|
||||||
|
class="button button-primary"
|
||||||
|
type="button"
|
||||||
|
:disabled="editing.size === 0"
|
||||||
|
@click="submit"
|
||||||
|
>Submit</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, inject } from 'vue';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
import EditActors from '#/components/edit/actors.vue';
|
||||||
|
import EditTags from '#/components/edit/tags.vue';
|
||||||
|
|
||||||
|
import { get, patch } from '#/src/api.js';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
|
||||||
|
const user = pageContext.user;
|
||||||
|
const scene = ref(pageContext.pageProps.scene);
|
||||||
|
|
||||||
|
// console.log(scene);
|
||||||
|
|
||||||
|
const fields = computed(() => [
|
||||||
|
{
|
||||||
|
key: 'actors',
|
||||||
|
type: 'actors',
|
||||||
|
value: scene.value.actors,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'tags',
|
||||||
|
type: 'tags',
|
||||||
|
value: scene.value.tags,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'title',
|
||||||
|
type: 'string',
|
||||||
|
value: scene.value.title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'description',
|
||||||
|
type: 'text',
|
||||||
|
value: scene.value.description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'date',
|
||||||
|
type: 'date',
|
||||||
|
value: scene.value.date
|
||||||
|
? format(scene.value.date, 'yyyy-MM-dd hh:mm')
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'duration',
|
||||||
|
type: 'duration',
|
||||||
|
value: [Math.floor(scene.value.duration / 3600), Math.floor((scene.value.duration % 3600) / 60), scene.value.duration % 60],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'productionDate',
|
||||||
|
label: 'production date',
|
||||||
|
type: 'date',
|
||||||
|
value: scene.value.productionDate
|
||||||
|
? format(scene.value.productionDate, 'yyyy-MM-dd hh:mm')
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
...(user.role === 'user'
|
||||||
|
? []
|
||||||
|
: [{
|
||||||
|
key: 'comment',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Do NOT use this field to summarize and clarify your revision. This field is for permanent notes and comments regarding the scene or database entry itself.',
|
||||||
|
value: scene.value.comment,
|
||||||
|
}]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const editing = ref(new Set());
|
||||||
|
const edits = ref({});
|
||||||
|
const comment = ref(null);
|
||||||
|
|
||||||
|
function toggleField(item) {
|
||||||
|
if (editing.value.has(item.key)) {
|
||||||
|
editing.value.delete(item.key);
|
||||||
|
delete edits.value[item.key];
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editing.value.add(item.key);
|
||||||
|
|
||||||
|
if (Array.isArray(item.value)) {
|
||||||
|
edits.value[item.key] = item.value.map((value) => value.hash || value.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
edits.value[item.key] = item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValue(item, event) {
|
||||||
|
edits.value[item.key] = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeUnits = ['h', 'm', 's'];
|
||||||
|
|
||||||
|
function setDuration(unit, event) {
|
||||||
|
edits.value.duration[timeUnits.indexOf(unit)] = Number(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
try {
|
||||||
|
await patch(`/scenes/${scene.value.id}`, {
|
||||||
|
edits: {
|
||||||
|
...edits.value,
|
||||||
|
duration: edits.value.duration
|
||||||
|
? (edits.value.duration[0] * 3600) + (edits.value.duration[1] * 60) + (edits.value.duration[2])
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
comment: comment.value,
|
||||||
|
}, {
|
||||||
|
successFeedback: 'Your revision has been submitted for approval.',
|
||||||
|
appendErrorMessage: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
editing.value = new Set();
|
||||||
|
edits.value = {};
|
||||||
|
comment.value = null;
|
||||||
|
|
||||||
|
scene.value = await get(`/scenes/${scene.value.id}`);
|
||||||
|
|
||||||
|
console.log(scene.value);
|
||||||
|
} catch (error) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
width: 8rem;
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--glass-strong-10);
|
||||||
|
background: none;
|
||||||
|
border: solid 1px var(--glass-weak-30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration {
|
||||||
|
.input {
|
||||||
|
width: 5rem;
|
||||||
|
margin-right: .25rem;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-actions {
|
||||||
|
.icon {
|
||||||
|
padding: .25rem 1rem;
|
||||||
|
fill: var(--glass);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem 1rem 0 1rem;
|
||||||
|
border-top: solid 1px var(--primary-light-30);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(--small) {
|
||||||
|
.row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-header {
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1 @@
|
||||||
|
export default '/scene/@sceneId/*/edit';
|
|
@ -112,6 +112,7 @@ export async function patch(path, data, options = {}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 204) {
|
if (res.status === 204) {
|
||||||
|
showFeedback(true, options);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import redis from './redis.js';
|
import redis from './redis.js';
|
||||||
|
|
||||||
export async function getIdsBySlug(slugs, domain) {
|
export async function getIdsBySlug(slugs, domain, toMap) {
|
||||||
if (!slugs) {
|
if (!slugs) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,9 @@ export async function getIdsBySlug(slugs, domain) {
|
||||||
return Number(id);
|
return Number(id);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (toMap) {
|
||||||
|
return Object.fromEntries(slugs.map((slug, index) => [slug, ids[index]]));
|
||||||
|
}
|
||||||
|
|
||||||
return ids.filter(Boolean);
|
return ids.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ export function curateMedia(media, context = {}) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: media.id,
|
id: media.id,
|
||||||
|
hash: media.hash,
|
||||||
path: media.path,
|
path: media.path,
|
||||||
thumbnail: media.thumbnail,
|
thumbnail: media.thumbnail,
|
||||||
lazy: media.lazy,
|
lazy: media.lazy,
|
||||||
|
|
181
src/scenes.js
181
src/scenes.js
|
@ -11,6 +11,9 @@ import { curateStash } from './stashes.js';
|
||||||
import { curateMedia } from './media.js';
|
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';
|
||||||
|
|
||||||
|
const logger = initLogger();
|
||||||
|
|
||||||
function getWatchUrl(scene) {
|
function getWatchUrl(scene) {
|
||||||
if (scene.url) {
|
if (scene.url) {
|
||||||
|
@ -64,6 +67,7 @@ function curateScene(rawScene, assets) {
|
||||||
description: rawScene.description,
|
description: rawScene.description,
|
||||||
duration: rawScene.duration,
|
duration: rawScene.duration,
|
||||||
shootId: rawScene.shoot_id,
|
shootId: rawScene.shoot_id,
|
||||||
|
productionDate: rawScene.production_date,
|
||||||
channel: {
|
channel: {
|
||||||
id: assets.channel.id,
|
id: assets.channel.id,
|
||||||
slug: assets.channel.slug,
|
slug: assets.channel.slug,
|
||||||
|
@ -595,3 +599,180 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||||
limit: options.limit,
|
limit: options.limit,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applySceneValueDelta(sceneId, delta, trx) {
|
||||||
|
console.log('value delta', delta);
|
||||||
|
|
||||||
|
return knexOwner('releases')
|
||||||
|
.where('id', sceneId)
|
||||||
|
.update(delta.key, delta.value)
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneActorsDelta(sceneId, delta, trx) {
|
||||||
|
console.log('actors delta', delta);
|
||||||
|
|
||||||
|
await knexOwner('releases_actors')
|
||||||
|
.where('release_id', sceneId)
|
||||||
|
.delete()
|
||||||
|
.transacting(trx);
|
||||||
|
|
||||||
|
await knexOwner('releases_actors')
|
||||||
|
.insert(delta.value.map((actorId) => ({
|
||||||
|
release_id: sceneId,
|
||||||
|
actor_id: actorId,
|
||||||
|
})))
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneTagsDelta(sceneId, delta, trx) {
|
||||||
|
console.log('tags delta', delta);
|
||||||
|
|
||||||
|
await knexOwner('releases_tags')
|
||||||
|
.where('release_id', sceneId)
|
||||||
|
.whereNotNull('tag_id')
|
||||||
|
.delete()
|
||||||
|
.transacting(trx);
|
||||||
|
|
||||||
|
await knexOwner('releases_tags')
|
||||||
|
.insert(delta.value.map((tagId) => ({
|
||||||
|
release_id: sceneId,
|
||||||
|
tag_id: tagId,
|
||||||
|
source: 'editor',
|
||||||
|
})))
|
||||||
|
.transacting(trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySceneRevision(sceneIds) {
|
||||||
|
const revisions = await knexOwner('scenes_revisions')
|
||||||
|
.whereIn('scene_id', sceneIds)
|
||||||
|
.whereNull('applied_at');
|
||||||
|
|
||||||
|
await revisions.reduce(async (chain, revision) => {
|
||||||
|
await chain;
|
||||||
|
|
||||||
|
console.log('revision', revision);
|
||||||
|
|
||||||
|
await knexOwner.transaction(async (trx) => {
|
||||||
|
await revision.deltas.map(async (delta) => {
|
||||||
|
if ([
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'date',
|
||||||
|
'duration',
|
||||||
|
'production_date',
|
||||||
|
'production_location',
|
||||||
|
'production_city',
|
||||||
|
'production_state',
|
||||||
|
].includes(delta.key)) {
|
||||||
|
return applySceneValueDelta(revision.scene_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'actors') {
|
||||||
|
return applySceneActorsDelta(revision.scene_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta.key === 'tags') {
|
||||||
|
return applySceneTagsDelta(revision.scene_id, delta, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await knexOwner('scenes_revisions')
|
||||||
|
.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 scene ${revision.scene_id}: ${error.message}`);
|
||||||
|
});
|
||||||
|
}, Promise.resolve());
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyMap = {
|
||||||
|
productionDate: 'production_date',
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createSceneRevision(sceneId, { edits, comment }, reqUser) {
|
||||||
|
const [
|
||||||
|
[scene],
|
||||||
|
openRevisions,
|
||||||
|
] = await Promise.all([
|
||||||
|
fetchScenesById([sceneId], { reqUser, includeAssets: true }),
|
||||||
|
knexOwner('scenes_revisions')
|
||||||
|
.where('user_id', reqUser.id)
|
||||||
|
.whereNull('approved_by')
|
||||||
|
.whereNot('failed', true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!scene) {
|
||||||
|
throw new HttpError(`No scene with ID ${sceneId} 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 baseScene = Object.fromEntries(Object.entries(scene).map(([key, values]) => {
|
||||||
|
if ([
|
||||||
|
'effectiveDate',
|
||||||
|
'isNew',
|
||||||
|
'network',
|
||||||
|
'stashes',
|
||||||
|
'watchUrl',
|
||||||
|
].includes(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.hash) {
|
||||||
|
return [key, values.hash];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values?.id) {
|
||||||
|
return [key, values.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (baseScene[key] === value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const valueSet = new Set(value);
|
||||||
|
const baseSet = new Set(baseScene[key]);
|
||||||
|
|
||||||
|
if (valueSet.size === baseSet.size && baseScene[key].every((id) => valueSet.has(id))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: keyMap[key] || key,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
if (deltas.length === 0) {
|
||||||
|
throw new HttpError('No effective changes provided', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await knexOwner('scenes_revisions').insert({
|
||||||
|
user_id: reqUser.id,
|
||||||
|
scene_id: scene.id,
|
||||||
|
base: JSON.stringify(baseScene),
|
||||||
|
deltas: JSON.stringify(deltas),
|
||||||
|
comment,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (['admin', 'editor'].includes(reqUser.role)) {
|
||||||
|
await applySceneRevision([scene.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -47,13 +47,17 @@ export async function fetchTags(options = {}) {
|
||||||
column: knex.raw('similarity(aliases.slug, :query)', { query }),
|
column: knex.raw('similarity(aliases.slug, :query)', { query }),
|
||||||
order: 'desc',
|
order: 'desc',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
column: 'aliases.priority',
|
||||||
|
order: 'desc',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
column: 'aliases.slug',
|
column: 'aliases.slug',
|
||||||
order: 'asc',
|
order: 'asc',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else if (!options.includeAliases) {
|
} else if (!options.includeAliases) {
|
||||||
builder.whereNull('alias_for');
|
builder.whereNull('tags.alias_for');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
knex('tags_posters')
|
knex('tags_posters')
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default async function mainHandler(req, res, next) {
|
||||||
id: req.user.id,
|
id: req.user.id,
|
||||||
username: req.user.username,
|
username: req.user.username,
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
|
role: req.user.role,
|
||||||
avatar: req.user.avatar,
|
avatar: req.user.avatar,
|
||||||
},
|
},
|
||||||
assets: req.user ? {
|
assets: req.user ? {
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
||||||
|
|
||||||
import { fetchScenes, fetchScenesById } from '../scenes.js';
|
import {
|
||||||
|
fetchScenes,
|
||||||
|
fetchScenesById,
|
||||||
|
createSceneRevision,
|
||||||
|
} from '../scenes.js';
|
||||||
|
|
||||||
import { parseActorIdentifier } from '../query.js';
|
import { parseActorIdentifier } from '../query.js';
|
||||||
import { getIdsBySlug } from '../cache.js';
|
import { getIdsBySlug } from '../cache.js';
|
||||||
import slugify from '../../utils/slugify.js';
|
import slugify from '../../utils/slugify.js';
|
||||||
|
import { HttpError } from '../errors.js';
|
||||||
import promiseProps from '../../utils/promise-props.js';
|
import promiseProps from '../../utils/promise-props.js';
|
||||||
|
|
||||||
export async function curateScenesQuery(query) {
|
export async function curateScenesQuery(query) {
|
||||||
|
@ -197,6 +203,18 @@ export async function fetchScenesGraphql(query, req) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchSceneApi(req, res) {
|
||||||
|
const [scene] = await fetchScenesById([Number(req.params.sceneId)], { reqUser: req.user });
|
||||||
|
|
||||||
|
console.log(req.params.sceneId, scene);
|
||||||
|
|
||||||
|
if (!scene) {
|
||||||
|
throw new HttpError(`No scene with ID ${req.params.sceneId} found`, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(scene);
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchScenesByIdGraphql(query, req) {
|
export async function fetchScenesByIdGraphql(query, req) {
|
||||||
const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), {
|
const scenes = await fetchScenesById([].concat(query.id, query.ids).filter(Boolean), {
|
||||||
reqUser: req.user,
|
reqUser: req.user,
|
||||||
|
@ -209,3 +227,9 @@ export async function fetchScenesByIdGraphql(query, req) {
|
||||||
|
|
||||||
return scenes[0];
|
return scenes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createSceneRevisionApi(req, res) {
|
||||||
|
await createSceneRevision(Number(req.params.sceneId), req.body, req.user);
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,12 @@ import redis from '../redis.js';
|
||||||
import errorHandler from './error.js';
|
import errorHandler from './error.js';
|
||||||
import consentHandler from './consent.js';
|
import consentHandler from './consent.js';
|
||||||
|
|
||||||
import { fetchScenesApi } from './scenes.js';
|
import {
|
||||||
|
fetchScenesApi,
|
||||||
|
fetchSceneApi,
|
||||||
|
createSceneRevisionApi,
|
||||||
|
} from './scenes.js';
|
||||||
|
|
||||||
import { fetchActorsApi } 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';
|
||||||
|
@ -179,6 +184,8 @@ export default async function initServer() {
|
||||||
|
|
||||||
// SCENES
|
// SCENES
|
||||||
router.get('/api/scenes', fetchScenesApi);
|
router.get('/api/scenes', fetchScenesApi);
|
||||||
|
router.get('/api/scenes/:sceneId', fetchSceneApi);
|
||||||
|
router.patch('/api/scenes/:sceneId', createSceneRevisionApi);
|
||||||
|
|
||||||
// ACTORS
|
// ACTORS
|
||||||
router.get('/api/actors', fetchActorsApi);
|
router.get('/api/actors', fetchActorsApi);
|
||||||
|
|
2
static
2
static
|
@ -1 +1 @@
|
||||||
Subproject commit 514a7accf3835913a7c168d34b996bde23dcf2d8
|
Subproject commit 7ed5e9579b65904738b1322c222f35d516cf52c5
|
|
@ -23,7 +23,7 @@ const propProcessors = {
|
||||||
.map((actor) => actor.name);
|
.map((actor) => actor.name);
|
||||||
},
|
},
|
||||||
tags: (sceneInfo, options) => sceneInfo.tags
|
tags: (sceneInfo, options) => sceneInfo.tags
|
||||||
.filter((tag) => {
|
?.filter((tag) => {
|
||||||
if (options.include && !options.include.includes(tag.slug)) {
|
if (options.include && !options.include.includes(tag.slug)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue