Added elaborate template switching.
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -2,7 +2,7 @@
|
|||
<Teleport to="#container">
|
||||
<div
|
||||
class="dialog-container"
|
||||
@click="emit('close')"
|
||||
@click="close"
|
||||
>
|
||||
<div
|
||||
class="dialog"
|
||||
|
@ -14,11 +14,11 @@
|
|||
<Icon
|
||||
icon="cross2"
|
||||
class="dialog-close"
|
||||
@click="emit('close')"
|
||||
@click="close"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
<slot @event="({ type, data }) => emit('event', { type, data })" />
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
@ -27,14 +27,24 @@
|
|||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
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'));
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,30 @@
|
|||
<template>
|
||||
<Dialog title="Edit summary template">
|
||||
<Dialog
|
||||
title="Edit summary template"
|
||||
:confirm-close="hasChanged"
|
||||
>
|
||||
<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
|
||||
ref="input"
|
||||
v-model="template"
|
||||
height="3"
|
||||
class="input edit"
|
||||
|
@ -26,39 +49,54 @@
|
|||
<button
|
||||
class="button"
|
||||
@click="reset"
|
||||
>Reset</button>
|
||||
>Default</button>
|
||||
</div>
|
||||
|
||||
<div class="actions save">
|
||||
<!--
|
||||
<form
|
||||
class="actions save"
|
||||
@submit.prevent="save"
|
||||
>
|
||||
<Icon
|
||||
v-if="selectedTemplate"
|
||||
icon="bin"
|
||||
class="remove"
|
||||
@click="remove"
|
||||
/>
|
||||
|
||||
<input
|
||||
v-model="templateName"
|
||||
class="input"
|
||||
placeholder="Name"
|
||||
required
|
||||
>
|
||||
-->
|
||||
|
||||
<button
|
||||
class="button"
|
||||
@click="save"
|
||||
>Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, inject } from 'vue';
|
||||
import { parse } from 'yaml';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
// import slugify from '#/utils/slugify.js';
|
||||
import events from '#/src/events.js';
|
||||
import { get, post, del } from '#/src/api.js';
|
||||
import processSummaryTemplate from '#/utils/process-summary-template.js';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
|
||||
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({
|
||||
write: (value) => value,
|
||||
});
|
||||
|
@ -68,20 +106,50 @@ const props = defineProps({
|
|||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
selected: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const storedTemplate = cookies.get('summary');
|
||||
const template = ref(storedTemplate ? JSON.parse(storedTemplate)?.custom : defaultTemplate);
|
||||
const templates = ref(pageContext.assets.templates);
|
||||
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 hasChanged = ref(false);
|
||||
const input = ref(null);
|
||||
|
||||
const templateName = ref(initialTemplate?.name || `custom_${Date.now()}`);
|
||||
|
||||
function getSummary() {
|
||||
return processSummaryTemplate(props.release, parse(template.value));
|
||||
return processSummaryTemplate(template.value, props.release);
|
||||
}
|
||||
|
||||
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() {
|
||||
hasError.value = false;
|
||||
hasChanged.value = true;
|
||||
|
||||
try {
|
||||
summary.value = getSummary();
|
||||
|
@ -90,24 +158,52 @@ function update() {
|
|||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
async function save() {
|
||||
try {
|
||||
parse(template.value);
|
||||
|
||||
cookies.set('summary', JSON.stringify({ custom: template.value }), { expires: 400 }); // 100 years from now
|
||||
hasChanged.value = false;
|
||||
|
||||
events.emit('feedback', {
|
||||
type: 'success',
|
||||
message: 'Saved summary template',
|
||||
const createdTemplate = await post('/templates', {
|
||||
name: templateName.value,
|
||||
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) {
|
||||
events.emit('feedback', {
|
||||
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() {
|
||||
navigator.clipboard.writeText(summary.value);
|
||||
|
||||
|
@ -118,7 +214,7 @@ function copy() {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
update();
|
||||
|
@ -176,11 +272,59 @@ function reset() {
|
|||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
.button:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&.save {
|
||||
flex-grow: 1;
|
||||
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>
|
||||
|
|
|
@ -141,9 +141,9 @@ const props = defineProps({
|
|||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<template #popper>
|
||||
<StashMenu
|
||||
:user="user"
|
||||
:stashes="stashes"
|
||||
:item-stashes="itemStashes"
|
||||
@stash="(stash) => stashItem(stash)"
|
||||
@unstash="(stash) => unstashItem(stash)"
|
||||
|
@ -53,10 +53,10 @@
|
|||
|
||||
<template v-else>
|
||||
<Icon
|
||||
v-if="itemStashes.some((itemStash) => itemStash.id === user.primaryStash.id)"
|
||||
v-if="itemStashes.some((itemStash) => itemStash.id === primaryStash.id)"
|
||||
icon="heart7"
|
||||
class="heart favorited noselect"
|
||||
@click.native.stop="unstashItem(user.primaryStash)"
|
||||
@click.native.stop="unstashItem(primaryStash)"
|
||||
@contextmenu.prevent="toggleShowStashes(true)"
|
||||
/>
|
||||
|
||||
|
@ -64,14 +64,14 @@
|
|||
v-else
|
||||
icon="heart8"
|
||||
class="heart noselect"
|
||||
@click.native.stop="stashItem(user.primaryStash)"
|
||||
@click.native.stop="stashItem(primaryStash)"
|
||||
@contextmenu.prevent="toggleShowStashes(true)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #popper>
|
||||
<StashMenu
|
||||
:user="user"
|
||||
:stashes="stashes"
|
||||
:item-stashes="itemStashes"
|
||||
@stash="(stash) => stashItem(stash)"
|
||||
@unstash="(stash) => unstashItem(stash)"
|
||||
|
@ -117,8 +117,10 @@ const emit = defineEmits(['stashed', 'unstashed']);
|
|||
|
||||
const pageContext = inject('pageContext');
|
||||
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 hasSecondaryStash = computed(() => itemStashes.value.some((itemStash) => !itemStash.isPrimary));
|
||||
|
||||
|
@ -195,9 +197,7 @@ function toggleShowStashes(state) {
|
|||
}
|
||||
|
||||
async function reloadStashes(newStash) {
|
||||
const profile = await get(`/users/${user.value.id}`);
|
||||
|
||||
user.value = profile;
|
||||
stashes.value = await get(`/users/${user.id}/stashes`);
|
||||
|
||||
await stashItem(newStash);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<ul class="stash-menu nolist noselect">
|
||||
<li
|
||||
v-for="userStash in user.stashes"
|
||||
v-for="userStash in stashes"
|
||||
:key="`stash-${userStash.id}`"
|
||||
class="menu-item"
|
||||
>
|
||||
|
@ -30,9 +30,9 @@
|
|||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
default: null,
|
||||
stashes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
itemStashes: {
|
||||
type: Array,
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</a>
|
||||
|
||||
<Icon
|
||||
v-if="!stash.public"
|
||||
v-if="!stash.isPublic"
|
||||
v-tooltip="'This stash is private'"
|
||||
icon="eye-blocked"
|
||||
class="private noselect"
|
||||
|
@ -31,7 +31,7 @@
|
|||
<template #popper>
|
||||
<ul class="stash-menu nolist">
|
||||
<li
|
||||
v-if="stash.public"
|
||||
v-if="stash.isPublic"
|
||||
class="menu-item"
|
||||
@click="setPublic(false)"
|
||||
>
|
||||
|
@ -158,7 +158,7 @@ async function setPublic(isPublic) {
|
|||
|
||||
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`,
|
||||
successFeedback: isPublic && `Stash '${props.stash.name}' set to public`,
|
||||
errorFeedback: 'Failed to update stash',
|
||||
|
|
|
@ -315,6 +315,7 @@
|
|||
>
|
||||
|
||||
<Icon
|
||||
v-if="user"
|
||||
v-tooltip="'Edit template'"
|
||||
icon="pencil5"
|
||||
class="edit"
|
||||
|
@ -328,6 +329,21 @@
|
|||
@click="copySummary"
|
||||
/>
|
||||
</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>
|
||||
|
@ -335,13 +351,16 @@
|
|||
<EditSummary
|
||||
v-if="showSummaryDialog"
|
||||
:release="scene"
|
||||
:selected="selectedTemplate"
|
||||
@close="showSummaryDialog = false"
|
||||
@event="({ type, data }) => type === 'select' && selectTemplate(data, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import { formatDate, formatDuration } from '#/utils/format.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 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 playing = ref(false);
|
||||
|
@ -391,16 +423,39 @@ const poster = computed(() => {
|
|||
return null;
|
||||
});
|
||||
|
||||
const summary = (() => {
|
||||
try {
|
||||
const result = processSummaryTemplate(scene);
|
||||
const summary = ref(null);
|
||||
const selectedTemplate = ref(null);
|
||||
|
||||
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) {
|
||||
console.error(`Failed to process summary template: ${error.message}`);
|
||||
return null;
|
||||
summary.value = null;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
selectTemplate(env.selectedTemplate);
|
||||
|
||||
function copySummary() {
|
||||
navigator.clipboard.writeText(summary);
|
||||
|
@ -728,7 +783,7 @@ function copySummary() {
|
|||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
.detail .icon {
|
||||
height: auto;
|
||||
padding: 0 .5rem 0 .75rem;
|
||||
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 {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -31,19 +31,19 @@
|
|||
|
||||
<StashDialog
|
||||
v-if="showStashDialog"
|
||||
@created="showStashDialog = false; reloadProfile();"
|
||||
@created="showStashDialog = false; reloadStashes();"
|
||||
@close="showStashDialog = false"
|
||||
/>
|
||||
|
||||
<ul class="stashes nolist">
|
||||
<li
|
||||
v-for="stash in profile.stashes"
|
||||
v-for="stash in stashes"
|
||||
:key="`stash-${stash.id}`"
|
||||
>
|
||||
<StashTile
|
||||
:stash="stash"
|
||||
:profile="profile"
|
||||
@reload="reloadProfile"
|
||||
@reload="reloadStashes"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -224,14 +224,16 @@ import AlertDialog from '#/components/alerts/create.vue';
|
|||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
const profile = ref(pageContext.pageProps.profile);
|
||||
const stashes = ref(pageContext.pageProps.stashes);
|
||||
const alerts = ref(pageContext.pageProps.alerts);
|
||||
|
||||
const done = ref(true);
|
||||
const showStashDialog = ref(false);
|
||||
const showAlertDialog = ref(false);
|
||||
|
||||
async function reloadProfile() {
|
||||
profile.value = await get(`/users/${profile.value.id}`);
|
||||
async function reloadStashes() {
|
||||
// profile.value = await get(`/users/${profile.value.id}`);
|
||||
stashes.value = await get(`/users/${profile.value.id}/stashes`);
|
||||
}
|
||||
|
||||
async function reloadAlerts() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
|
||||
import { fetchUser } from '#/src/users.js';
|
||||
import { fetchUserStashes } from '#/src/stashes.js';
|
||||
import { fetchAlerts } from '#/src/alerts.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
|
@ -15,11 +16,14 @@ export async function onBeforeRender(pageContext) {
|
|||
throw render(404, `Cannot find user '${pageContext.routeParams.username}'.`);
|
||||
}
|
||||
|
||||
const stashes = await fetchUserStashes(profile.id, pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: profile.username,
|
||||
pageProps: {
|
||||
profile, // differentiate from authed 'user'
|
||||
stashes,
|
||||
alerts,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@ export default {
|
|||
'urlParsed',
|
||||
'env',
|
||||
'user',
|
||||
'assets',
|
||||
'campaigns',
|
||||
'meta',
|
||||
],
|
||||
|
|
|
@ -44,10 +44,9 @@ export async function login(credentials, userIp) {
|
|||
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,
|
||||
raw: true,
|
||||
includeStashes: true,
|
||||
}).catch(() => {
|
||||
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
|
||||
return curateUser(user, { stashes });
|
||||
return curateUser(user);
|
||||
}
|
||||
|
||||
export async function signup(credentials, userIp) {
|
||||
|
|
|
@ -24,7 +24,7 @@ export function curateStash(stash, assets = {}) {
|
|||
name: stash.name,
|
||||
slug: stash.slug,
|
||||
isPrimary: stash.primary,
|
||||
public: stash.public,
|
||||
isPublic: stash.public,
|
||||
createdAt: stash.created_at,
|
||||
stashedScenes: stash.stashed_scenes ?? null,
|
||||
stashedMovies: stash.stashed_movies ?? null,
|
||||
|
@ -45,7 +45,7 @@ function curateStashEntry(stash, user) {
|
|||
user_id: user?.id || undefined,
|
||||
name: stash.name || undefined,
|
||||
slug: slugify(stash.name) || undefined,
|
||||
public: stash.public ?? false,
|
||||
public: stash.isPublic ?? false,
|
||||
};
|
||||
|
||||
return curatedStashEntry;
|
||||
|
@ -86,7 +86,21 @@ export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUs
|
|||
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`)
|
||||
.select('stashes.*')
|
||||
.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`)
|
||||
|
@ -233,7 +247,7 @@ export async function stashActor(actorId, stashId, sessionUser) {
|
|||
|
||||
refreshView('actors');
|
||||
|
||||
return fetchStashes('actor', actorId, sessionUser);
|
||||
return fetchDomainStashes('actor', actorId, sessionUser);
|
||||
}
|
||||
|
||||
export async function unstashActor(actorId, stashId, sessionUser) {
|
||||
|
@ -268,7 +282,7 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
|||
|
||||
refreshView('actors');
|
||||
|
||||
return fetchStashes('actor', actorId, sessionUser);
|
||||
return fetchDomainStashes('actor', actorId, sessionUser);
|
||||
}
|
||||
|
||||
export async function stashScene(sceneId, stashId, sessionUser) {
|
||||
|
@ -297,7 +311,7 @@ export async function stashScene(sceneId, stashId, sessionUser) {
|
|||
|
||||
refreshView('scenes');
|
||||
|
||||
return fetchStashes('scene', sceneId, sessionUser);
|
||||
return fetchDomainStashes('scene', sceneId, sessionUser);
|
||||
}
|
||||
|
||||
export async function unstashScene(sceneId, stashId, sessionUser) {
|
||||
|
@ -328,7 +342,7 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
|
|||
|
||||
refreshView('scenes');
|
||||
|
||||
return fetchStashes('scene', sceneId, sessionUser);
|
||||
return fetchDomainStashes('scene', sceneId, sessionUser);
|
||||
}
|
||||
|
||||
export async function stashMovie(movieId, stashId, sessionUser) {
|
||||
|
@ -356,7 +370,7 @@ export async function stashMovie(movieId, stashId, sessionUser) {
|
|||
|
||||
refreshView('movies');
|
||||
|
||||
return fetchStashes('movie', movieId, sessionUser);
|
||||
return fetchDomainStashes('movie', movieId, sessionUser);
|
||||
}
|
||||
|
||||
export async function unstashMovie(movieId, stashId, sessionUser) {
|
||||
|
@ -387,7 +401,7 @@ export async function unstashMovie(movieId, stashId, sessionUser) {
|
|||
|
||||
refreshView('movies');
|
||||
|
||||
return fetchStashes('movie', movieId, sessionUser);
|
||||
return fetchDomainStashes('movie', movieId, sessionUser);
|
||||
}
|
||||
|
||||
CronJob.from({
|
||||
|
|
95
src/users.js
|
@ -1,13 +1,24 @@
|
|||
import { parse } from 'yaml';
|
||||
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import { curateStash } from './stashes.js';
|
||||
// import { curateStash } from './stashes.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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [];
|
||||
// const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [];
|
||||
|
||||
const curatedUser = {
|
||||
id: user.id,
|
||||
|
@ -17,8 +28,6 @@ export function curateUser(user, assets = {}) {
|
|||
identityVerified: user.identity_verified,
|
||||
avatar: `/media/avatars/${user.id}_${user.username}.png`,
|
||||
createdAt: user.created_at,
|
||||
stashes: curatedStashes,
|
||||
primaryStash: curatedStashes.find((stash) => stash.isPrimary),
|
||||
};
|
||||
|
||||
return curatedUser;
|
||||
|
@ -38,7 +47,7 @@ function whereUser(builder, userId, options = {}) {
|
|||
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')
|
||||
.select(knex.raw('users.*, users_roles.abilities as role_abilities'))
|
||||
.modify((builder) => whereUser(builder, userId, options))
|
||||
|
@ -50,19 +59,71 @@ export async function fetchUser(userId, options = {}, reqUser) {
|
|||
throw new HttpError(`User '${userId}' not found`, 404);
|
||||
}
|
||||
|
||||
const stashes = await knex('stashes')
|
||||
.select('stashes.*', 'stashes_meta.*')
|
||||
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
|
||||
.where('user_id', user.id)
|
||||
.modify((builder) => {
|
||||
if (reqUser?.id !== user.id && !options.includeStashes) {
|
||||
builder.where('public', true);
|
||||
}
|
||||
});
|
||||
/*
|
||||
const [stashes, templates] = await Promise.all([
|
||||
knex('stashes')
|
||||
.select('stashes.*', 'stashes_meta.*')
|
||||
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
|
||||
.where('user_id', user.id)
|
||||
.modify((builder) => {
|
||||
if (reqUser?.id !== user.id && !options.includeStashes) {
|
||||
builder.where('public', true);
|
||||
}
|
||||
}),
|
||||
options.includeTemplates
|
||||
? knex('users_templates').where('user_id', user.id)
|
||||
: null,
|
||||
]);
|
||||
*/
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { login, signup } from '../auth.js';
|
|||
import { fetchUser } from '../users.js';
|
||||
|
||||
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('.')
|
||||
? ip.slice(ip.lastIndexOf(':') + 1)
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -7,7 +7,6 @@ import session from 'express-session';
|
|||
import RedisStore from 'connect-redis';
|
||||
import compression from 'compression';
|
||||
import cookie from 'cookie';
|
||||
import { renderPage } from 'vike/server'; // eslint-disable-line import/extensions
|
||||
|
||||
import redis from '../redis.js';
|
||||
|
||||
|
@ -22,6 +21,8 @@ import { fetchTagsApi } from './tags.js';
|
|||
|
||||
import { graphqlApi } from './graphql.js';
|
||||
|
||||
import mainHandler from './main.js';
|
||||
|
||||
import {
|
||||
setUserApi,
|
||||
loginApi,
|
||||
|
@ -31,9 +32,13 @@ import {
|
|||
|
||||
import {
|
||||
fetchUserApi,
|
||||
fetchUserTemplatesApi,
|
||||
createTemplateApi,
|
||||
removeTemplateApi,
|
||||
} from './users.js';
|
||||
|
||||
import {
|
||||
fetchUserStashesApi,
|
||||
createStashApi,
|
||||
removeStashApi,
|
||||
stashActorApi,
|
||||
|
@ -54,8 +59,6 @@ import {
|
|||
updateNotificationsApi,
|
||||
} from './alerts.js';
|
||||
|
||||
import { fetchUnseenNotificationsCount } from '../alerts.js';
|
||||
|
||||
import initLogger from '../logger.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
@ -141,6 +144,7 @@ export default async function initServer() {
|
|||
router.patch('/api/users/:userId/notifications/:notificationId', updateNotificationApi);
|
||||
|
||||
// STASHES
|
||||
router.get('/api/users/:userId/stashes', fetchUserStashesApi);
|
||||
router.post('/api/stashes', createStashApi);
|
||||
router.patch('/api/stashes/:stashId', updateStashApi);
|
||||
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/movies/:movieId', unstashMovieApi);
|
||||
|
||||
// SUMMARY TEMPLATES
|
||||
router.get('/api/users/:userId/templates', fetchUserTemplatesApi);
|
||||
router.post('/api/templates', createTemplateApi);
|
||||
router.delete('/api/templates/:templateId', removeTemplateApi);
|
||||
|
||||
// ALERTS
|
||||
router.get('/api/alerts', fetchAlertsApi);
|
||||
router.post('/api/alerts', createAlertApi);
|
||||
|
@ -186,61 +195,7 @@ export default async function initServer() {
|
|||
next();
|
||||
});
|
||||
|
||||
router.get('*', async (req, res, next) => {
|
||||
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.get('*', mainHandler);
|
||||
|
||||
router.use(errorHandler);
|
||||
app.use(router);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
fetchUserStashes,
|
||||
createStash,
|
||||
removeStash,
|
||||
stashActor,
|
||||
|
@ -10,27 +11,26 @@ import {
|
|||
updateStash,
|
||||
} 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) {
|
||||
const stash = await createStash(req.body, req.session.user);
|
||||
|
||||
await updateSessionUser(req);
|
||||
const stash = await createStash(req.body, req.user);
|
||||
|
||||
res.send(stash);
|
||||
}
|
||||
|
||||
export async function updateStashApi(req, res) {
|
||||
const stash = await updateStash(Number(req.params.stashId), req.body, req.session.user);
|
||||
|
||||
await updateSessionUser(req);
|
||||
const stash = await updateStash(Number(req.params.stashId), req.body, req.user);
|
||||
|
||||
res.send(stash);
|
||||
}
|
||||
|
||||
export async function removeStashApi(req, res) {
|
||||
await removeStash(Number(req.params.stashId), req.session.user);
|
||||
await updateSessionUser(req);
|
||||
await removeStash(Number(req.params.stashId), req.user);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -1,19 +1,8 @@
|
|||
import { format } from 'date-fns';
|
||||
import Cookies from 'js-cookie';
|
||||
import { parse } from 'yaml';
|
||||
|
||||
import slugify from '#/utils/slugify.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;
|
||||
import ellipsis from '#/utils/ellipsis.js';
|
||||
|
||||
const genderMap = {
|
||||
f: 'female',
|
||||
|
@ -35,11 +24,11 @@ const propProcessors = {
|
|||
},
|
||||
tags: (sceneInfo, options) => sceneInfo.tags
|
||||
.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;
|
||||
}
|
||||
|
||||
if (options.exclude?.includes(tag.name) || options.exclude?.includes(tag.slug)) {
|
||||
if (options.exclude?.includes(tag.slug)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -54,32 +43,58 @@ const propProcessors = {
|
|||
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 key = typeof item === 'string' ? item : item.key;
|
||||
const keys = typeof item === 'string' ? item : item.key;
|
||||
|
||||
if (key) {
|
||||
const value = propProcessors[key]?.(release, typeof item === 'string' ? { key } : item) || release[key];
|
||||
if ((item.channels && !item.channels.includes(release.channel?.slug)) || item.notChannels?.includes(release.channel?.slug)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return result.concat(value.join(item.delimit || ', '));
|
||||
}
|
||||
if ((item.networks && !item.networks.includes(release.network?.slug)) || item.notNetworks?.includes(release.network?.slug)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return result.concat(item.slugify ? slugify(value, item.slugify) : value);
|
||||
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) {
|
||||
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) {
|
||||
return `${wrapOpen}${results.filter(Boolean).join(delimit)}${wrapClose}`;
|
||||
return `${wrap[0] || ''}${results.filter(Boolean).join(delimit)}${wrap[1] || ''}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function processSummaryTemplate(template, release) {
|
||||
const chain = parse(template);
|
||||
|
||||
return traverseTemplate(chain, release);
|
||||
}
|
||||
|
|