Added elaborate template switching.

This commit is contained in:
DebaucheryLibrarian 2024-08-26 06:15:22 +02:00
parent fa991c0294
commit 80d8a8109a
29 changed files with 617 additions and 180 deletions

View File

@ -0,0 +1,4 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM6.707 10.293c0.391 0.391 0.391 1.024 0 1.414-0.195 0.195-0.451 0.293-0.707 0.293s-0.512-0.098-0.707-0.293l-3-3c-0.391-0.391-0.391-1.024 0-1.414l3-3c0.391-0.391 1.024-0.391 1.414 0s0.391 1.024 0 1.414l-2.293 2.293 2.293 2.293zM10.707 11.707c-0.391 0.391-1.024 0.391-1.414 0s-0.391-1.024 0-1.414l2.293-2.293-2.293-2.293c-0.391-0.391-0.391-1.024 0-1.414 0.195-0.195 0.451-0.293 0.707-0.293s0.512 0.098 0.707 0.293l3 3c0.391 0.391 0.391 1.024 0 1.414l-3 3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 672 B

View File

@ -0,0 +1,4 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM7 4.111c-0.552 0-1 0.497-1 1.111v1.111c0 0.92-0.672 1.667-1.5 1.667 0.828 0 1.5 0.746 1.5 1.667v1.111c0 0.614 0.448 1.111 1 1.111v1.111h-1c-1.103 0-2-0.997-2-2.222v-1.111c0-0.614-0.448-1.111-1-1.111v-1.111c0.552 0 1-0.497 1-1.111v-1.111c0-1.225 0.897-2.222 2-2.222h1v1.111zM13 8.556c-0.552 0-1 0.498-1 1.111v1.111c0 1.225-0.897 2.222-2 2.222h-1v-1.111c0.552 0 1-0.497 1-1.111v-1.111c0-0.92 0.672-1.667 1.5-1.667-0.828 0-1.5-0.746-1.5-1.667v-1.111c0-0.614-0.448-1.111-1-1.111v-1.111h1c1.103 0 2 0.997 2 2.222v1.111c0 0.614 0.448 1.111 1 1.111v1.111z"></path>
</svg>

After

Width:  |  Height:  |  Size: 768 B

5
assets/img/icons/embed.svg Executable file
View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M9 11.5l1.5 1.5 5-5-5-5-1.5 1.5 3.5 3.5z"></path>
<path d="M7 4.5l-1.5-1.5-5 5 5 5 1.5-1.5-3.5-3.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 256 B

6
assets/img/icons/embed2.svg Executable file
View File

@ -0,0 +1,6 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 20 16">
<path d="M13 11.5l1.5 1.5 5-5-5-5-1.5 1.5 3.5 3.5z"></path>
<path d="M7 4.5l-1.5-1.5-5 5 5 5 1.5-1.5-3.5-3.5z"></path>
<path d="M10.958 2.352l1.085 0.296-3 11-1.085-0.296 3-11z"></path>
</svg>

After

Width:  |  Height:  |  Size: 324 B

5
assets/img/icons/file-css2.svg Executable file
View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M11 4h3.627c-0.078-0.126-0.172-0.266-0.286-0.421-0.347-0.473-0.831-1.027-1.362-1.558s-1.085-1.015-1.558-1.362c-0.155-0.114-0.295-0.208-0.421-0.286v3.627z"></path>
<path d="M10.5 5c-0.276 0-0.5-0.224-0.5-0.5v-4.5h-7.75c-0.689 0-1.25 0.561-1.25 1.25v13.5c0 0.689 0.561 1.25 1.25 1.25h11.5c0.689 0 1.25-0.561 1.25-1.25v-9.75h-4.5zM6 11.25v1.25c0 0.276 0.224 0.5 0.5 0.5s0.5 0.224 0.5 0.5-0.224 0.5-0.5 0.5c-0.827 0-1.5-0.673-1.5-1.5v-1.25c0-0.138-0.112-0.25-0.25-0.25h-0.25c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h0.25c0.138 0 0.25-0.112 0.25-0.25v-1.25c0-0.827 0.673-1.5 1.5-1.5 0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5c-0.276 0-0.5 0.224-0.5 0.5v1.25c0 0.281-0.093 0.541-0.251 0.75 0.157 0.209 0.251 0.469 0.251 0.75zM11.5 11h-0.25c-0.138 0-0.25 0.112-0.25 0.25v1.25c0 0.827-0.673 1.5-1.5 1.5-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5c0.276 0 0.5-0.224 0.5-0.5v-1.25c0-0.281 0.093-0.541 0.251-0.75-0.157-0.209-0.251-0.469-0.251-0.75v-1.25c0-0.276-0.224-0.5-0.5-0.5s-0.5-0.224-0.5-0.5 0.224-0.5 0.5-0.5c0.827 0 1.5 0.673 1.5 1.5v1.25c0 0.138 0.112 0.25 0.25 0.25h0.25c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

6
assets/img/icons/file-eye2.svg Executable file
View File

@ -0,0 +1,6 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M11 4h3.627c-0.078-0.126-0.172-0.266-0.286-0.421-0.347-0.473-0.831-1.027-1.362-1.558s-1.085-1.015-1.558-1.362c-0.155-0.114-0.295-0.208-0.421-0.286v3.627z"></path>
<path d="M5.755 15.881c-1.241-0.806-2.19-1.932-2.743-3.254-0.167-0.399-0.167-0.856 0-1.254 0.553-1.322 1.502-2.448 2.743-3.254 1.253-0.814 2.721-1.244 4.245-1.244s2.992 0.43 4.245 1.244c0.266 0.172 0.518 0.36 0.755 0.56v-3.679h-4.5c-0.276 0-0.5-0.224-0.5-0.5v-4.5h-7.75c-0.689 0-1.25 0.561-1.25 1.25v13.5c0 0.689 0.561 1.25 1.25 1.25h3.695c-0.064-0.039-0.127-0.078-0.19-0.119z"></path>
<path d="M15.95 11.807c-0.466-1.113-1.267-2.062-2.318-2.745-1.070-0.695-2.326-1.062-3.632-1.062s-2.562 0.367-3.632 1.062c-1.051 0.683-1.853 1.632-2.318 2.745-0.052 0.123-0.052 0.262 0 0.386 0.466 1.113 1.267 2.062 2.318 2.745 1.070 0.695 2.326 1.062 3.632 1.062s2.562-0.367 3.632-1.062c1.051-0.683 1.853-1.632 2.318-2.745 0.052-0.123 0.052-0.262-0-0.386zM10 11c0 0.552-0.448 1-1 1s-1-0.448-1-1 0.448-1 1-1 1 0.448 1 1zM13.087 14.099c-0.908 0.589-1.975 0.901-3.087 0.901s-2.18-0.312-3.087-0.901c-0.82-0.533-1.458-1.255-1.855-2.099 0.397-0.844 1.035-1.566 1.855-2.099 0.102-0.066 0.206-0.128 0.311-0.188-0.199 0.405-0.311 0.86-0.311 1.342 0 1.681 1.363 3.044 3.044 3.044s3.044-1.363 3.044-3.044c0-0.508-0.125-0.986-0.344-1.407 0.147 0.078 0.292 0.162 0.432 0.253 0.82 0.533 1.457 1.255 1.855 2.099-0.397 0.844-1.035 1.566-1.855 2.099v0 0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M11 4h3.627c-0.078-0.126-0.172-0.266-0.286-0.421-0.347-0.473-0.831-1.027-1.362-1.558s-1.085-1.015-1.558-1.362c-0.155-0.114-0.295-0.208-0.421-0.286v3.627z"></path>
<path d="M10.5 5c-0.276 0-0.5-0.224-0.5-0.5v-4.5h-7.75c-0.689 0-1.25 0.561-1.25 1.25v13.5c0 0.689 0.561 1.25 1.25 1.25h11.5c0.689 0 1.25-0.561 1.25-1.25v-9.75h-4.5zM11.5 13h-7c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h7c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5zM11.5 11h-7c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h7c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5zM11.5 9h-7c-0.276 0-0.5-0.224-0.5-0.5s0.224-0.5 0.5-0.5h7c0.276 0 0.5 0.224 0.5 0.5s-0.224 0.5-0.5 0.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 795 B

5
assets/img/icons/file-xml2.svg Executable file
View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M11 4h3.627c-0.078-0.126-0.172-0.266-0.286-0.421-0.347-0.473-0.831-1.027-1.362-1.558s-1.085-1.015-1.558-1.362c-0.155-0.114-0.295-0.208-0.421-0.286v3.627z"></path>
<path d="M10.5 5c-0.276 0-0.5-0.224-0.5-0.5v-4.5h-7.75c-0.689 0-1.25 0.561-1.25 1.25v13.5c0 0.689 0.561 1.25 1.25 1.25h11.5c0.689 0 1.25-0.561 1.25-1.25v-9.75h-4.5zM7.5 13l-1 1-3-3 3-3 1 1-2 2 2 2zM9.5 14l-1-1 2-2-2-2 1-1 3 3-3 3z"></path>
</svg>

After

Width:  |  Height:  |  Size: 550 B

View File

@ -0,0 +1,8 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M5 2h-2c-0.55 0-1 0.45-1 1v2c0 0.55 0.45 1 1 1h2c0.55 0 1-0.45 1-1v-2c0-0.55-0.45-1-1-1z"></path>
<path d="M11 6h2c0.55 0 1-0.45 1-1v-2c0-0.55-0.45-1-1-1h-2c-0.55 0-1 0.45-1 1v2c0 0.55 0.45 1 1 1zM11 3h2v2h-2v-2z"></path>
<path d="M5 10h-2c-0.55 0-1 0.45-1 1v2c0 0.55 0.45 1 1 1h2c0.55 0 1-0.45 1-1v-2c0-0.55-0.45-1-1-1zM5 13h-2v-2h2v2z"></path>
<path d="M13 10h-2c-0.55 0-1 0.45-1 1v2c0 0.55 0.45 1 1 1h2c0.55 0 1-0.45 1-1v-2c0-0.55-0.45-1-1-1z"></path>
<path d="M14 8h-1c-1.336 0-2.591-0.52-3.536-1.464s-1.464-2.2-1.464-3.536v-1c0-1.1-0.9-2-2-2h-4c-1.1 0-2 0.9-2 2v4c0 1.1 0.9 2 2 2h1c1.336 0 2.591 0.52 3.536 1.464s1.464 2.2 1.464 3.536v1c0 1.1 0.9 2 2 2h4c1.1 0 2-0.9 2-2v-4c0-1.1-0.9-2-2-2zM15 14c0 0.265-0.105 0.515-0.295 0.705s-0.44 0.295-0.705 0.295h-4c-0.265 0-0.515-0.105-0.705-0.295s-0.295-0.44-0.295-0.705v-1c0-3.314-2.686-6-6-6h-1c-0.265 0-0.515-0.105-0.705-0.295s-0.295-0.441-0.295-0.705v-4c0-0.265 0.105-0.515 0.295-0.705s0.44-0.295 0.705-0.295h4c0.265 0 0.515 0.105 0.705 0.295s0.295 0.44 0.295 0.705v1c0 3.314 2.686 6 6 6h1c0.265 0 0.515 0.105 0.705 0.295s0.295 0.44 0.295 0.705v4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

6
assets/img/icons/markup.svg Executable file
View File

@ -0,0 +1,6 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="16" viewBox="0 0 24 16">
<path d="M17 7v-2h-2v-2h-2v2h-2v-2h-2v2h-2v2h2v2h-2v2h2v2h2v-2h2v2h2v-2h2v-2h-2v-2h2zM13 9h-2v-2h2v2z"></path>
<path d="M8.707 1.707l-1.414-1.414-7.293 7.293v0.828l7.293 7.293 1.414-1.414-6.293-6.293z"></path>
<path d="M15.293 1.707l1.414-1.414 7.293 7.293v0.828l-7.293 7.293-1.414-1.414 6.293-6.293z"></path>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@ -2,7 +2,7 @@
<Teleport to="#container"> <Teleport to="#container">
<div <div
class="dialog-container" class="dialog-container"
@click="emit('close')" @click="close"
> >
<div <div
class="dialog" class="dialog"
@ -14,11 +14,11 @@
<Icon <Icon
icon="cross2" icon="cross2"
class="dialog-close" class="dialog-close"
@click="emit('close')" @click="close"
/> />
</div> </div>
<slot /> <slot @event="({ type, data }) => emit('event', { type, data })" />
</div> </div>
</div> </div>
</Teleport> </Teleport>
@ -27,14 +27,24 @@
<script setup> <script setup>
import { onMounted } from 'vue'; import { onMounted } from 'vue';
defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: null, default: null,
}, },
confirmClose: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['open', 'close']); const emit = defineEmits(['open', 'close', 'event']);
function close() {
if (!props.confirmClose || confirm('You have unchanged changes, are you sure you want to close the dialog?')) { // eslint-disable-line no-restricted-globals, no-alert
emit('close');
}
}
onMounted(() => emit('open')); onMounted(() => emit('open'));
</script> </script>

View File

@ -1,7 +1,30 @@
<template> <template>
<Dialog title="Edit summary template"> <Dialog
title="Edit summary template"
:confirm-close="hasChanged"
>
<div class="dialog-body"> <div class="dialog-body">
<ul class="templates nolist">
<li
v-for="storedTemplate in templates"
:key="`template-${storedTemplate.id}`"
class="template-key"
:class="{ selected: selectedTemplate === storedTemplate.id }"
@click="selectTemplate(storedTemplate.id)"
>{{ storedTemplate.name }}</li>
<li
class="template-key add"
@click="add"
>
<Icon
icon="plus3"
/>
</li>
</ul>
<textarea <textarea
ref="input"
v-model="template" v-model="template"
height="3" height="3"
class="input edit" class="input edit"
@ -26,39 +49,54 @@
<button <button
class="button" class="button"
@click="reset" @click="reset"
>Reset</button> >Default</button>
</div> </div>
<div class="actions save"> <form
<!-- class="actions save"
@submit.prevent="save"
>
<Icon
v-if="selectedTemplate"
icon="bin"
class="remove"
@click="remove"
/>
<input <input
v-model="templateName"
class="input" class="input"
placeholder="Name" placeholder="Name"
required
> >
-->
<button <button
class="button" class="button"
@click="save"
>Save</button> >Save</button>
</div> </form>
</div> </div>
</div> </div>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, inject } from 'vue';
import { parse } from 'yaml'; import { parse } from 'yaml';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
// import slugify from '#/utils/slugify.js';
import events from '#/src/events.js'; import events from '#/src/events.js';
import { get, post, del } from '#/src/api.js';
import processSummaryTemplate from '#/utils/process-summary-template.js'; import processSummaryTemplate from '#/utils/process-summary-template.js';
import Dialog from '#/components/dialog/dialog.vue'; import Dialog from '#/components/dialog/dialog.vue';
import defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved import defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved
const emit = defineEmits(['event']);
const pageContext = inject('pageContext');
const cookies = Cookies.withConverter({ const cookies = Cookies.withConverter({
write: (value) => value, write: (value) => value,
}); });
@ -68,20 +106,50 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
selected: {
type: Number,
default: null,
},
}); });
const storedTemplate = cookies.get('summary'); const templates = ref(pageContext.assets.templates);
const template = ref(storedTemplate ? JSON.parse(storedTemplate)?.custom : defaultTemplate); const selectedTemplate = ref(props.selected || templates.value.at(0)?.id || null);
const initialTemplate = templates.value.find((storedTemplate) => storedTemplate.id === selectedTemplate.value) || null;
const template = ref(initialTemplate?.template || defaultTemplate);
const hasError = ref(false); const hasError = ref(false);
const hasChanged = ref(false);
const input = ref(null);
const templateName = ref(initialTemplate?.name || `custom_${Date.now()}`);
function getSummary() { function getSummary() {
return processSummaryTemplate(props.release, parse(template.value)); return processSummaryTemplate(template.value, props.release);
} }
const summary = ref(getSummary()); const summary = ref(getSummary());
function selectTemplate(templateId) {
selectedTemplate.value = templateId;
const nextTemplate = templates.value.find((storedTemplate) => storedTemplate.id === templateId);
template.value = nextTemplate.template;
templateName.value = nextTemplate.name;
summary.value = getSummary();
emit('event', {
type: 'select',
data: templateId,
});
cookies.set('selectedTemplate', String(templateId));
}
function update() { function update() {
hasError.value = false; hasError.value = false;
hasChanged.value = true;
try { try {
summary.value = getSummary(); summary.value = getSummary();
@ -90,24 +158,52 @@ function update() {
} }
} }
function save() { async function save() {
try { try {
parse(template.value); parse(template.value);
cookies.set('summary', JSON.stringify({ custom: template.value }), { expires: 400 }); // 100 years from now hasChanged.value = false;
events.emit('feedback', { const createdTemplate = await post('/templates', {
type: 'success', name: templateName.value,
message: 'Saved summary template', template: template.value,
successFeedback: `Saved summary template '${templateName.value}'`,
errorFeedback: `Failed to save summary template '${templateName.value}'`,
}); });
templates.value = await get(`/users/${pageContext.user.id}/templates`);
selectTemplate(createdTemplate.id);
} catch (error) { } catch (error) {
events.emit('feedback', { events.emit('feedback', {
type: 'error', type: 'error',
message: `Failed to save summary template: ${error.message}`, message: `Failed to save summary template '${templateName.value}': ${error.message}`,
}); });
} }
} }
function add() {
selectedTemplate.value = null;
template.value = '';
templateName.value = `custom_${Date.now()}`;
summary.value = '';
input.value.focus();
}
async function remove() {
if (confirm(`Are you sure you want to delete summary template ${templateName.value}?`)) { // eslint-disable-line no-restricted-globals, no-alert
await del(`/templates/${selectedTemplate.value}`, {
undoFeedback: `Deleted summary template '${templateName.value}'`,
errorFeedback: `Failed to remove summary template '${templateName.value}'`,
});
templates.value = await get(`/users/${pageContext.user.id}/templates`);
selectTemplate(templates.value.at(-1)?.id);
}
}
function copy() { function copy() {
navigator.clipboard.writeText(summary.value); navigator.clipboard.writeText(summary.value);
@ -118,7 +214,7 @@ function copy() {
} }
function reset() { function reset() {
if (confirm('Are you sure you want to reset the summary template? Your custom template will be discarded.')) { // eslint-disable-line no-restricted-globals, no-alert if (confirm('Are you sure you want to reset the summary template to the default? Your custom template will be discarded.')) { // eslint-disable-line no-restricted-globals, no-alert
template.value = defaultTemplate; template.value = defaultTemplate;
update(); update();
@ -176,11 +272,59 @@ function reset() {
.actions { .actions {
display: flex; display: flex;
gap: 1rem;
.button:not(:last-child) {
margin-right: 1rem;
}
&.save { &.save {
flex-grow: 1; flex-grow: 1;
justify-content: flex-end; justify-content: flex-end;
.input {
margin-right: 1rem;
}
}
.icon {
height: 100%;
padding: 0 1rem;
cursor: pointer;
fill: var(--glass);
&.remove:hover {
fill: var(--error);
}
}
}
.templates {
display: flex;
align-items: center;
padding: .5rem;
overflow-x: auto;
}
.template-key {
padding: .25rem .5rem;
border-radius: .25rem;
cursor: pointer;
.icon {
fill: var(--glass);
}
&.selected {
background: var(--primary);
color: var(--text-light);
}
&:hover:not(.selected) {
color: var(--primary);
.icon {
fill: var(--primary);
}
} }
} }
</style> </style>

View File

@ -141,9 +141,9 @@ const props = defineProps({
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const user = pageContext.user; const user = pageContext.user;
const pageStash = pageContext.pageProps.stash; const pageStash = pageContext.pageProps.stash;
const currentStash = pageStash || user?.primaryStash; const currentStash = pageStash || pageContext.assets.primaryStash;
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash.id)); const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash?.id));
</script> </script>
<style scoped> <style scoped>

View File

@ -17,7 +17,7 @@
<template #popper> <template #popper>
<StashMenu <StashMenu
:user="user" :stashes="stashes"
:item-stashes="itemStashes" :item-stashes="itemStashes"
@stash="(stash) => stashItem(stash)" @stash="(stash) => stashItem(stash)"
@unstash="(stash) => unstashItem(stash)" @unstash="(stash) => unstashItem(stash)"
@ -53,10 +53,10 @@
<template v-else> <template v-else>
<Icon <Icon
v-if="itemStashes.some((itemStash) => itemStash.id === user.primaryStash.id)" v-if="itemStashes.some((itemStash) => itemStash.id === primaryStash.id)"
icon="heart7" icon="heart7"
class="heart favorited noselect" class="heart favorited noselect"
@click.native.stop="unstashItem(user.primaryStash)" @click.native.stop="unstashItem(primaryStash)"
@contextmenu.prevent="toggleShowStashes(true)" @contextmenu.prevent="toggleShowStashes(true)"
/> />
@ -64,14 +64,14 @@
v-else v-else
icon="heart8" icon="heart8"
class="heart noselect" class="heart noselect"
@click.native.stop="stashItem(user.primaryStash)" @click.native.stop="stashItem(primaryStash)"
@contextmenu.prevent="toggleShowStashes(true)" @contextmenu.prevent="toggleShowStashes(true)"
/> />
</template> </template>
<template #popper> <template #popper>
<StashMenu <StashMenu
:user="user" :stashes="stashes"
:item-stashes="itemStashes" :item-stashes="itemStashes"
@stash="(stash) => stashItem(stash)" @stash="(stash) => stashItem(stash)"
@unstash="(stash) => unstashItem(stash)" @unstash="(stash) => unstashItem(stash)"
@ -117,8 +117,10 @@ const emit = defineEmits(['stashed', 'unstashed']);
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const pageStash = pageContext.pageProps.stash; const pageStash = pageContext.pageProps.stash;
const user = pageContext.user;
const user = ref(pageContext.user); const stashes = ref(pageContext.assets?.stashes);
const primaryStash = pageContext.assets?.primaryStash;
const itemStashes = ref(props.item.stashes); const itemStashes = ref(props.item.stashes);
const hasSecondaryStash = computed(() => itemStashes.value.some((itemStash) => !itemStash.isPrimary)); const hasSecondaryStash = computed(() => itemStashes.value.some((itemStash) => !itemStash.isPrimary));
@ -195,9 +197,7 @@ function toggleShowStashes(state) {
} }
async function reloadStashes(newStash) { async function reloadStashes(newStash) {
const profile = await get(`/users/${user.value.id}`); stashes.value = await get(`/users/${user.id}/stashes`);
user.value = profile;
await stashItem(newStash); await stashItem(newStash);
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<ul class="stash-menu nolist noselect"> <ul class="stash-menu nolist noselect">
<li <li
v-for="userStash in user.stashes" v-for="userStash in stashes"
:key="`stash-${userStash.id}`" :key="`stash-${userStash.id}`"
class="menu-item" class="menu-item"
> >
@ -30,9 +30,9 @@
import Checkbox from '#/components/form/checkbox.vue'; import Checkbox from '#/components/form/checkbox.vue';
defineProps({ defineProps({
user: { stashes: {
type: Object, type: Array,
default: null, default: () => [],
}, },
itemStashes: { itemStashes: {
type: Array, type: Array,

View File

@ -15,7 +15,7 @@
</a> </a>
<Icon <Icon
v-if="!stash.public" v-if="!stash.isPublic"
v-tooltip="'This stash is private'" v-tooltip="'This stash is private'"
icon="eye-blocked" icon="eye-blocked"
class="private noselect" class="private noselect"
@ -31,7 +31,7 @@
<template #popper> <template #popper>
<ul class="stash-menu nolist"> <ul class="stash-menu nolist">
<li <li
v-if="stash.public" v-if="stash.isPublic"
class="menu-item" class="menu-item"
@click="setPublic(false)" @click="setPublic(false)"
> >
@ -158,7 +158,7 @@ async function setPublic(isPublic) {
done.value = false; done.value = false;
await patch(`/stashes/${props.stash.id}`, { public: isPublic }, { await patch(`/stashes/${props.stash.id}`, { isPublic }, {
undoFeedback: !isPublic && `Stash '${props.stash.name}' set to private`, undoFeedback: !isPublic && `Stash '${props.stash.name}' set to private`,
successFeedback: isPublic && `Stash '${props.stash.name}' set to public`, successFeedback: isPublic && `Stash '${props.stash.name}' set to public`,
errorFeedback: 'Failed to update stash', errorFeedback: 'Failed to update stash',

View File

@ -315,6 +315,7 @@
> >
<Icon <Icon
v-if="user"
v-tooltip="'Edit template'" v-tooltip="'Edit template'"
icon="pencil5" icon="pencil5"
class="edit" class="edit"
@ -328,6 +329,21 @@
@click="copySummary" @click="copySummary"
/> />
</div> </div>
<ul
v-if="user && assets.templates.length > 0"
class="nolist templates"
>
<Icon icon="markup" />
<li
v-for="userTemplate in templates"
:key="`template-${userTemplate.id}`"
class="template"
:class="{ selected: userTemplate.id === selectedTemplate }"
@click="selectTemplate(userTemplate.id)"
>{{ userTemplate.name }}</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
@ -335,13 +351,16 @@
<EditSummary <EditSummary
v-if="showSummaryDialog" v-if="showSummaryDialog"
:release="scene" :release="scene"
:selected="selectedTemplate"
@close="showSummaryDialog = false" @close="showSummaryDialog = false"
@event="({ type, data }) => type === 'select' && selectTemplate(data, false)"
/> />
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, inject } from 'vue'; import { ref, computed, inject } from 'vue';
import Cookies from 'js-cookie';
import { formatDate, formatDuration } from '#/utils/format.js'; import { formatDate, formatDuration } from '#/utils/format.js';
import events from '#/src/events.js'; import events from '#/src/events.js';
@ -356,7 +375,20 @@ import Heart from '#/components/stashes/heart.vue';
import Campaign from '#/components/campaigns/campaign.vue'; import Campaign from '#/components/campaigns/campaign.vue';
import EditSummary from '#/components/scenes/edit-summary.vue'; import EditSummary from '#/components/scenes/edit-summary.vue';
const { pageProps, campaigns } = inject('pageContext'); import defaultTemplate from '#/assets/summary.yaml?raw'; // eslint-disable-line import/no-unresolved
const cookies = Cookies.withConverter({
write: (value) => value,
});
const {
pageProps,
campaigns,
user,
assets,
env,
} = inject('pageContext');
const { scene } = pageProps; const { scene } = pageProps;
const playing = ref(false); const playing = ref(false);
@ -391,16 +423,39 @@ const poster = computed(() => {
return null; return null;
}); });
const summary = (() => { const summary = ref(null);
try { const selectedTemplate = ref(null);
const result = processSummaryTemplate(scene);
return result; const templates = [
{
id: 0,
name: 'traxxx',
template: defaultTemplate,
},
...(assets?.templates || []),
];
function selectTemplate(templateId, allowFallback = true) {
try {
const targetTemplate = templates.find((userTemplate) => userTemplate.id === templateId);
if (!targetTemplate && !allowFallback) {
return;
}
const template = targetTemplate || templates[0];
summary.value = processSummaryTemplate(template.template, scene);
selectedTemplate.value = template.id;
cookies.set('selectedTemplate', String(templateId));
} catch (error) { } catch (error) {
console.error(`Failed to process summary template: ${error.message}`); console.error(`Failed to process summary template: ${error.message}`);
return null; summary.value = null;
} }
})(); }
selectTemplate(env.selectedTemplate);
function copySummary() { function copySummary() {
navigator.clipboard.writeText(summary); navigator.clipboard.writeText(summary);
@ -728,7 +783,7 @@ function copySummary() {
flex-grow: 1; flex-grow: 1;
} }
.icon { .detail .icon {
height: auto; height: auto;
padding: 0 .5rem 0 .75rem; padding: 0 .5rem 0 .75rem;
fill: var(--glass); fill: var(--glass);
@ -740,6 +795,34 @@ function copySummary() {
} }
} }
.templates {
display: flex;
align-items: center;
margin-top: .5rem;
.icon {
width: 1.2rem;
height: 1.2rem;
margin-right: .5rem;
fill: var(--glass-weak-10);
}
}
.template {
padding: .25rem .5rem;
border-radius: .25rem;
cursor: pointer;
&:hover {
color: var(--primary);
}
&.selected {
background: var(--primary);
color: var(--text-light);
}
}
.compact-show { .compact-show {
display: none; display: none;
} }

View File

@ -31,19 +31,19 @@
<StashDialog <StashDialog
v-if="showStashDialog" v-if="showStashDialog"
@created="showStashDialog = false; reloadProfile();" @created="showStashDialog = false; reloadStashes();"
@close="showStashDialog = false" @close="showStashDialog = false"
/> />
<ul class="stashes nolist"> <ul class="stashes nolist">
<li <li
v-for="stash in profile.stashes" v-for="stash in stashes"
:key="`stash-${stash.id}`" :key="`stash-${stash.id}`"
> >
<StashTile <StashTile
:stash="stash" :stash="stash"
:profile="profile" :profile="profile"
@reload="reloadProfile" @reload="reloadStashes"
/> />
</li> </li>
</ul> </ul>
@ -224,14 +224,16 @@ import AlertDialog from '#/components/alerts/create.vue';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
const user = pageContext.user; const user = pageContext.user;
const profile = ref(pageContext.pageProps.profile); const profile = ref(pageContext.pageProps.profile);
const stashes = ref(pageContext.pageProps.stashes);
const alerts = ref(pageContext.pageProps.alerts); const alerts = ref(pageContext.pageProps.alerts);
const done = ref(true); const done = ref(true);
const showStashDialog = ref(false); const showStashDialog = ref(false);
const showAlertDialog = ref(false); const showAlertDialog = ref(false);
async function reloadProfile() { async function reloadStashes() {
profile.value = await get(`/users/${profile.value.id}`); // profile.value = await get(`/users/${profile.value.id}`);
stashes.value = await get(`/users/${profile.value.id}/stashes`);
} }
async function reloadAlerts() { async function reloadAlerts() {

View File

@ -1,6 +1,7 @@
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */ import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { fetchUser } from '#/src/users.js'; import { fetchUser } from '#/src/users.js';
import { fetchUserStashes } from '#/src/stashes.js';
import { fetchAlerts } from '#/src/alerts.js'; import { fetchAlerts } from '#/src/alerts.js';
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
@ -15,11 +16,14 @@ export async function onBeforeRender(pageContext) {
throw render(404, `Cannot find user '${pageContext.routeParams.username}'.`); throw render(404, `Cannot find user '${pageContext.routeParams.username}'.`);
} }
const stashes = await fetchUserStashes(profile.id, pageContext.user);
return { return {
pageContext: { pageContext: {
title: profile.username, title: profile.username,
pageProps: { pageProps: {
profile, // differentiate from authed 'user' profile, // differentiate from authed 'user'
stashes,
alerts, alerts,
}, },
}, },

View File

@ -6,6 +6,7 @@ export default {
'urlParsed', 'urlParsed',
'env', 'env',
'user', 'user',
'assets',
'campaigns', 'campaigns',
'meta', 'meta',
], ],

View File

@ -44,10 +44,9 @@ export async function login(credentials, userIp) {
throw new HttpError('Logins are currently disabled', 405); throw new HttpError('Logins are currently disabled', 405);
} }
const { user, stashes } = await fetchUser(credentials.username.trim(), { const { user } = await fetchUser(credentials.username.trim(), {
email: true, email: true,
raw: true, raw: true,
includeStashes: true,
}).catch(() => { }).catch(() => {
throw new HttpError('Username or password incorrect', 401); throw new HttpError('Username or password incorrect', 401);
}); });
@ -67,7 +66,7 @@ export async function login(credentials, userIp) {
} }
// fetched the raw user for password verification, don't return directly to user // fetched the raw user for password verification, don't return directly to user
return curateUser(user, { stashes }); return curateUser(user);
} }
export async function signup(credentials, userIp) { export async function signup(credentials, userIp) {

View File

@ -24,7 +24,7 @@ export function curateStash(stash, assets = {}) {
name: stash.name, name: stash.name,
slug: stash.slug, slug: stash.slug,
isPrimary: stash.primary, isPrimary: stash.primary,
public: stash.public, isPublic: stash.public,
createdAt: stash.created_at, createdAt: stash.created_at,
stashedScenes: stash.stashed_scenes ?? null, stashedScenes: stash.stashed_scenes ?? null,
stashedMovies: stash.stashed_movies ?? null, stashedMovies: stash.stashed_movies ?? null,
@ -45,7 +45,7 @@ function curateStashEntry(stash, user) {
user_id: user?.id || undefined, user_id: user?.id || undefined,
name: stash.name || undefined, name: stash.name || undefined,
slug: slugify(stash.name) || undefined, slug: slugify(stash.name) || undefined,
public: stash.public ?? false, public: stash.isPublic ?? false,
}; };
return curatedStashEntry; return curatedStashEntry;
@ -86,7 +86,21 @@ export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUs
return curateStash(stash, { user }); return curateStash(stash, { user });
} }
export async function fetchStashes(domain, itemId, sessionUser) { export async function fetchUserStashes(userId, reqUser) {
const stashes = await knex('stashes')
.select('stashes.*', 'stashes_meta.*')
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
.where('user_id', userId)
.modify((builder) => {
if (userId !== reqUser?.id) {
builder.where('public', true);
}
});
return stashes.map((stash) => curateStash(stash, { user: reqUser }));
}
export async function fetchDomainStashes(domain, itemId, sessionUser) {
const stashes = await knex(`stashes_${domain}s`) const stashes = await knex(`stashes_${domain}s`)
.select('stashes.*') .select('stashes.*')
.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`) .leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`)
@ -233,7 +247,7 @@ export async function stashActor(actorId, stashId, sessionUser) {
refreshView('actors'); refreshView('actors');
return fetchStashes('actor', actorId, sessionUser); return fetchDomainStashes('actor', actorId, sessionUser);
} }
export async function unstashActor(actorId, stashId, sessionUser) { export async function unstashActor(actorId, stashId, sessionUser) {
@ -268,7 +282,7 @@ export async function unstashActor(actorId, stashId, sessionUser) {
refreshView('actors'); refreshView('actors');
return fetchStashes('actor', actorId, sessionUser); return fetchDomainStashes('actor', actorId, sessionUser);
} }
export async function stashScene(sceneId, stashId, sessionUser) { export async function stashScene(sceneId, stashId, sessionUser) {
@ -297,7 +311,7 @@ export async function stashScene(sceneId, stashId, sessionUser) {
refreshView('scenes'); refreshView('scenes');
return fetchStashes('scene', sceneId, sessionUser); return fetchDomainStashes('scene', sceneId, sessionUser);
} }
export async function unstashScene(sceneId, stashId, sessionUser) { export async function unstashScene(sceneId, stashId, sessionUser) {
@ -328,7 +342,7 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
refreshView('scenes'); refreshView('scenes');
return fetchStashes('scene', sceneId, sessionUser); return fetchDomainStashes('scene', sceneId, sessionUser);
} }
export async function stashMovie(movieId, stashId, sessionUser) { export async function stashMovie(movieId, stashId, sessionUser) {
@ -356,7 +370,7 @@ export async function stashMovie(movieId, stashId, sessionUser) {
refreshView('movies'); refreshView('movies');
return fetchStashes('movie', movieId, sessionUser); return fetchDomainStashes('movie', movieId, sessionUser);
} }
export async function unstashMovie(movieId, stashId, sessionUser) { export async function unstashMovie(movieId, stashId, sessionUser) {
@ -387,7 +401,7 @@ export async function unstashMovie(movieId, stashId, sessionUser) {
refreshView('movies'); refreshView('movies');
return fetchStashes('movie', movieId, sessionUser); return fetchDomainStashes('movie', movieId, sessionUser);
} }
CronJob.from({ CronJob.from({

View File

@ -1,13 +1,24 @@
import { parse } from 'yaml';
import { knexOwner as knex } from './knex.js'; import { knexOwner as knex } from './knex.js';
import { curateStash } from './stashes.js'; // import { curateStash } from './stashes.js';
import { HttpError } from './errors.js'; import { HttpError } from './errors.js';
export function curateUser(user, assets = {}) { function curateTemplate(template) {
return {
id: template.id,
name: template.name,
template: template.template,
createdAt: template.created_at,
};
}
export function curateUser(user, _assets = {}) {
if (!user) { if (!user) {
return null; return null;
} }
const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || []; // const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [];
const curatedUser = { const curatedUser = {
id: user.id, id: user.id,
@ -17,8 +28,6 @@ export function curateUser(user, assets = {}) {
identityVerified: user.identity_verified, identityVerified: user.identity_verified,
avatar: `/media/avatars/${user.id}_${user.username}.png`, avatar: `/media/avatars/${user.id}_${user.username}.png`,
createdAt: user.created_at, createdAt: user.created_at,
stashes: curatedStashes,
primaryStash: curatedStashes.find((stash) => stash.isPrimary),
}; };
return curatedUser; return curatedUser;
@ -38,7 +47,7 @@ function whereUser(builder, userId, options = {}) {
builder.where('users.id', Number(userId)); builder.where('users.id', Number(userId));
} }
export async function fetchUser(userId, options = {}, reqUser) { export async function fetchUser(userId, options = {}, _reqUser) {
const user = await knex('users') const user = await knex('users')
.select(knex.raw('users.*, users_roles.abilities as role_abilities')) .select(knex.raw('users.*, users_roles.abilities as role_abilities'))
.modify((builder) => whereUser(builder, userId, options)) .modify((builder) => whereUser(builder, userId, options))
@ -50,7 +59,9 @@ export async function fetchUser(userId, options = {}, reqUser) {
throw new HttpError(`User '${userId}' not found`, 404); throw new HttpError(`User '${userId}' not found`, 404);
} }
const stashes = await knex('stashes') /*
const [stashes, templates] = await Promise.all([
knex('stashes')
.select('stashes.*', 'stashes_meta.*') .select('stashes.*', 'stashes_meta.*')
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id') .leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
.where('user_id', user.id) .where('user_id', user.id)
@ -58,11 +69,61 @@ export async function fetchUser(userId, options = {}, reqUser) {
if (reqUser?.id !== user.id && !options.includeStashes) { if (reqUser?.id !== user.id && !options.includeStashes) {
builder.where('public', true); builder.where('public', true);
} }
}); }),
options.includeTemplates
? knex('users_templates').where('user_id', user.id)
: null,
]);
*/
if (options.raw) { if (options.raw) {
return { user, stashes }; // return { user, stashes, templates };
return { user };
} }
return curateUser(user, { stashes }); // return curateUser(user, { stashes, templates });
return curateUser(user, {});
}
export async function fetchUserTemplates(reqUser) {
const templates = await knex('users_templates')
.where('user_id', reqUser.id)
.orderBy('created_at', 'asc');
return templates.map((template) => curateTemplate(template));
}
export async function createTemplate(template, reqUser) {
if (!template.template) {
throw new HttpError('No template specified', 400);
}
if (!template.name) {
throw new HttpError('No template name specified', 400);
}
try {
parse(template.template);
} catch (error) {
throw new HttpError(`Invalid YAML: ${error.message}`, 400);
}
const [templateEntry] = await knex('users_templates')
.insert({
user_id: reqUser.id,
name: template.name,
template: template.template,
})
.onConflict(['name', 'user_id'])
.merge()
.returning('*');
return curateTemplate(templateEntry);
}
export async function removeTemplate(templateId, reqUser) {
await knex('users_templates')
.where('id', templateId)
.where('user_id', reqUser.id)
.delete();
} }

View File

@ -5,7 +5,7 @@ import { login, signup } from '../auth.js';
import { fetchUser } from '../users.js'; import { fetchUser } from '../users.js';
function getIp(req) { function getIp(req) {
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress; // See src/ws const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;
const unmappedIp = ip?.includes('.') const unmappedIp = ip?.includes('.')
? ip.slice(ip.lastIndexOf(':') + 1) ? ip.slice(ip.lastIndexOf(':') + 1)

70
src/web/main.js Normal file
View File

@ -0,0 +1,70 @@
import config from 'config';
import { renderPage } from 'vike/server'; // eslint-disable-line import/extensions
import { fetchUserStashes } from '../stashes.js';
import { fetchUserTemplates } from '../users.js';
import { fetchUnseenNotificationsCount } from '../alerts.js';
export default async function mainHandler(req, res, next) {
const [stashes, templates, unseenNotifications] = req.user && await Promise.all([
fetchUserStashes(req.user.id, req.user),
fetchUserTemplates(req.user),
fetchUnseenNotificationsCount(req.user),
]);
const pageContextInit = {
urlOriginal: req.originalUrl,
urlQuery: req.query, // vike's own query does not apply boolean parser
headers: req.headers,
cookies: req.cookies,
tagFilter: req.tagFilter,
user: req.user && {
id: req.user.id,
username: req.user.username,
email: req.user.email,
avatar: req.user.avatar,
},
assets: req.user ? {
stashes,
primaryStash: stashes.find((stash) => stash.isPrimary),
templates,
} : null,
env: {
theme: req.cookies.theme || req.headers['sec-ch-prefers-color-scheme'] || 'light',
selectedTemplate: Number(req.cookies.selectedTemplate) || 0,
allowLogin: config.auth.login,
allowSignup: config.auth.signup,
maxMatches: config.database.manticore.maxMatches,
maxAggregateSize: config.database.manticore.maxAggregateSize,
media: config.media,
psa: config.psa,
links: config.links,
},
meta: {
unseenNotifications,
},
};
const pageContext = await renderPage(pageContextInit);
const { httpResponse } = pageContext;
if (pageContext.errorWhileRendering) {
console.error(pageContext.errorWhileRendering);
}
if (!httpResponse) {
next();
return;
}
/*
if (res.writeEarlyHints) {
res.writeEarlyHints({ link: httpResponse.earlyHints.map((e) => e.earlyHintLink) });
}
*/
httpResponse.headers.forEach(([name, value]) => res.setHeader(name, value));
res.status(httpResponse.statusCode);
// For HTTP streams use httpResponse.pipe() instead, see https://vike.dev/stream
res.send(httpResponse.body);
}

View File

@ -7,7 +7,6 @@ import session from 'express-session';
import RedisStore from 'connect-redis'; import RedisStore from 'connect-redis';
import compression from 'compression'; import compression from 'compression';
import cookie from 'cookie'; import cookie from 'cookie';
import { renderPage } from 'vike/server'; // eslint-disable-line import/extensions
import redis from '../redis.js'; import redis from '../redis.js';
@ -22,6 +21,8 @@ import { fetchTagsApi } from './tags.js';
import { graphqlApi } from './graphql.js'; import { graphqlApi } from './graphql.js';
import mainHandler from './main.js';
import { import {
setUserApi, setUserApi,
loginApi, loginApi,
@ -31,9 +32,13 @@ import {
import { import {
fetchUserApi, fetchUserApi,
fetchUserTemplatesApi,
createTemplateApi,
removeTemplateApi,
} from './users.js'; } from './users.js';
import { import {
fetchUserStashesApi,
createStashApi, createStashApi,
removeStashApi, removeStashApi,
stashActorApi, stashActorApi,
@ -54,8 +59,6 @@ import {
updateNotificationsApi, updateNotificationsApi,
} from './alerts.js'; } from './alerts.js';
import { fetchUnseenNotificationsCount } from '../alerts.js';
import initLogger from '../logger.js'; import initLogger from '../logger.js';
const logger = initLogger(); const logger = initLogger();
@ -141,6 +144,7 @@ export default async function initServer() {
router.patch('/api/users/:userId/notifications/:notificationId', updateNotificationApi); router.patch('/api/users/:userId/notifications/:notificationId', updateNotificationApi);
// STASHES // STASHES
router.get('/api/users/:userId/stashes', fetchUserStashesApi);
router.post('/api/stashes', createStashApi); router.post('/api/stashes', createStashApi);
router.patch('/api/stashes/:stashId', updateStashApi); router.patch('/api/stashes/:stashId', updateStashApi);
router.delete('/api/stashes/:stashId', removeStashApi); router.delete('/api/stashes/:stashId', removeStashApi);
@ -153,6 +157,11 @@ export default async function initServer() {
router.delete('/api/stashes/:stashId/scenes/:sceneId', unstashSceneApi); router.delete('/api/stashes/:stashId/scenes/:sceneId', unstashSceneApi);
router.delete('/api/stashes/:stashId/movies/:movieId', unstashMovieApi); router.delete('/api/stashes/:stashId/movies/:movieId', unstashMovieApi);
// SUMMARY TEMPLATES
router.get('/api/users/:userId/templates', fetchUserTemplatesApi);
router.post('/api/templates', createTemplateApi);
router.delete('/api/templates/:templateId', removeTemplateApi);
// ALERTS // ALERTS
router.get('/api/alerts', fetchAlertsApi); router.get('/api/alerts', fetchAlertsApi);
router.post('/api/alerts', createAlertApi); router.post('/api/alerts', createAlertApi);
@ -186,61 +195,7 @@ export default async function initServer() {
next(); next();
}); });
router.get('*', async (req, res, next) => { router.get('*', mainHandler);
const unseenNotifications = await fetchUnseenNotificationsCount(req.user);
const pageContextInit = {
urlOriginal: req.originalUrl,
urlQuery: req.query, // vike's own query does not apply boolean parser
headers: req.headers,
cookies: req.cookies,
tagFilter: req.tagFilter,
user: req.user && {
id: req.user.id,
username: req.user.username,
email: req.user.email,
avatar: req.user.avatar,
stashes: req.user.stashes,
primaryStash: req.user.primaryStash,
},
env: {
theme: req.cookies.theme || req.headers['sec-ch-prefers-color-scheme'] || 'light',
allowLogin: config.auth.login,
allowSignup: config.auth.signup,
maxMatches: config.database.manticore.maxMatches,
maxAggregateSize: config.database.manticore.maxAggregateSize,
media: config.media,
psa: config.psa,
links: config.links,
},
meta: {
unseenNotifications,
},
};
const pageContext = await renderPage(pageContextInit);
const { httpResponse } = pageContext;
if (pageContext.errorWhileRendering) {
console.error(pageContext.errorWhileRendering);
}
if (!httpResponse) {
next();
return;
}
/*
if (res.writeEarlyHints) {
res.writeEarlyHints({ link: httpResponse.earlyHints.map((e) => e.earlyHintLink) });
}
*/
httpResponse.headers.forEach(([name, value]) => res.setHeader(name, value));
res.status(httpResponse.statusCode);
// For HTTP streams use httpResponse.pipe() instead, see https://vike.dev/stream
res.send(httpResponse.body);
});
router.use(errorHandler); router.use(errorHandler);
app.use(router); app.use(router);

View File

@ -1,4 +1,5 @@
import { import {
fetchUserStashes,
createStash, createStash,
removeStash, removeStash,
stashActor, stashActor,
@ -10,27 +11,26 @@ import {
updateStash, updateStash,
} from '../stashes.js'; } from '../stashes.js';
import { updateSessionUser } from './auth.js'; export async function fetchUserStashesApi(req, res) {
const stashes = await fetchUserStashes(req.user.id, req.user);
res.send(stashes);
}
export async function createStashApi(req, res) { export async function createStashApi(req, res) {
const stash = await createStash(req.body, req.session.user); const stash = await createStash(req.body, req.user);
await updateSessionUser(req);
res.send(stash); res.send(stash);
} }
export async function updateStashApi(req, res) { export async function updateStashApi(req, res) {
const stash = await updateStash(Number(req.params.stashId), req.body, req.session.user); const stash = await updateStash(Number(req.params.stashId), req.body, req.user);
await updateSessionUser(req);
res.send(stash); res.send(stash);
} }
export async function removeStashApi(req, res) { export async function removeStashApi(req, res) {
await removeStash(Number(req.params.stashId), req.session.user); await removeStash(Number(req.params.stashId), req.user);
await updateSessionUser(req);
res.status(204).send(); res.status(204).send();
} }

View File

@ -1,7 +1,32 @@
import { fetchUser } from '../users.js'; import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
import {
fetchUser,
fetchUserTemplates,
createTemplate,
removeTemplate,
} from '../users.js';
export async function fetchUserApi(req, res) { export async function fetchUserApi(req, res) {
const user = await fetchUser(req.params.userId, {}, req.user); const user = await fetchUser(req.params.userId, {}, req.user);
res.send(user); res.send(stringify(user));
}
export async function fetchUserTemplatesApi(req, res) {
const templates = await fetchUserTemplates(req.user);
res.send(templates);
}
export async function createTemplateApi(req, res) {
const template = await createTemplate(req.body, req.user);
res.send(stringify(template));
}
export async function removeTemplateApi(req, res) {
await removeTemplate(req.params.templateId, req.user);
res.status(204).send();
} }

View File

@ -1,19 +1,8 @@
import { format } from 'date-fns'; import { format } from 'date-fns';
import Cookies from 'js-cookie';
import { parse } from 'yaml'; import { parse } from 'yaml';
import slugify from '#/utils/slugify.js'; import slugify from '#/utils/slugify.js';
import ellipsis from '#/utils/ellipsis.js';
import defaultTemplate from '#/assets/summary.yaml';
const cookies = Cookies.withConverter({
write: (value) => value,
});
const storedTemplate = cookies.get('summary');
const template = storedTemplate
? parse(JSON.parse(storedTemplate)?.custom)
: defaultTemplate;
const genderMap = { const genderMap = {
f: 'female', f: 'female',
@ -35,11 +24,11 @@ const propProcessors = {
}, },
tags: (sceneInfo, options) => sceneInfo.tags tags: (sceneInfo, options) => sceneInfo.tags
.filter((tag) => { .filter((tag) => {
if (options.include && !options.include.includes(tag.name) && !options.include.includes(tag.slug)) { if (options.include && !options.include.includes(tag.slug)) {
return false; return false;
} }
if (options.exclude?.includes(tag.name) || options.exclude?.includes(tag.slug)) { if (options.exclude?.includes(tag.slug)) {
return false; return false;
} }
@ -54,32 +43,58 @@ const propProcessors = {
date: (sceneInfo, options) => format(sceneInfo.effectiveDate, options.format || 'yyyy-MM-dd'), date: (sceneInfo, options) => format(sceneInfo.effectiveDate, options.format || 'yyyy-MM-dd'),
}; };
export default function processReleaseTemplate(release, chain = template, delimit = ' ', wrapOpen = '', wrapClose = '') { function curateValue(value, item) {
return [].concat(value) // account for both arrays (actors, tags) and strings (title, channel)
.slice(0, item.limit || Infinity)
.map((listValue) => (item.slugify ? slugify(listValue, item.slugify) : listValue))
.map((listValue) => ellipsis(listValue, item.slice || Infinity, item.ellipsis || ''))
.join(item.delimit || ', ');
}
function traverseTemplate(chain, release, {
delimit = ' ',
wrap = ['', ''],
} = {}) {
const results = chain.reduce((result, item) => { const results = chain.reduce((result, item) => {
const key = typeof item === 'string' ? item : item.key; const keys = typeof item === 'string' ? item : item.key;
if (key) { if ((item.channels && !item.channels.includes(release.channel?.slug)) || item.notChannels?.includes(release.channel?.slug)) {
const value = propProcessors[key]?.(release, typeof item === 'string' ? { key } : item) || release[key]; return result;
if (Array.isArray(value)) {
return result.concat(value.join(item.delimit || ', '));
} }
return result.concat(item.slugify ? slugify(value, item.slugify) : value); if ((item.networks && !item.networks.includes(release.network?.slug)) || item.notNetworks?.includes(release.network?.slug)) {
return result;
}
if (keys) {
const value = keys.split('|').reduce((acc, key) => acc
|| propProcessors[key]?.(release, typeof item === 'string' ? { key } : item)
|| release[key], null);
return result.concat(curateValue(value, item));
} }
if (item.items) { if (item.items) {
const value = processReleaseTemplate(release, item.items, item.delimit, item.wrap?.[0] || '', item.wrap?.[1] || ''); const group = traverseTemplate(item.items, release, {
delimit: item.delimit,
wrap: item.wrap,
});
return result.concat(item.slugify ? slugify(value, item.slugify) : value); return result.concat(curateValue(group, item));
} }
return []; return result;
}, []); }, []);
if (results.length > 0) { if (results.length > 0) {
return `${wrapOpen}${results.filter(Boolean).join(delimit)}${wrapClose}`; return `${wrap[0] || ''}${results.filter(Boolean).join(delimit)}${wrap[1] || ''}`;
} }
return ''; return '';
} }
export default function processSummaryTemplate(template, release) {
const chain = parse(template);
return traverseTemplate(chain, release);
}