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">
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}

View File

@ -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,

View File

@ -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',

View File

@ -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;
}

View File

@ -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() {

View File

@ -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,
},
},

View File

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

View File

@ -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) {

View File

@ -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({

View File

@ -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();
}

View File

@ -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)

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 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);

View File

@ -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();
}

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) {
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 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);
}