Added summary template editor, improved summary template format and options.
This commit is contained in:
parent
d2c9b447ee
commit
e2cffbdde2
|
@ -29,6 +29,7 @@
|
|||
--background-level-20: #eee;
|
||||
--background-level-30: #eee;
|
||||
--background-dim: var(--shadow-weak-10);
|
||||
--background-error: rgba(255, 0, 0, .1);
|
||||
|
||||
--shadow-weak-50: rgba(0, 0, 0, .02);
|
||||
--shadow-weak-40: rgba(0, 0, 0, .05);
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
- ' - ':
|
||||
- delimit: ' - '
|
||||
items:
|
||||
- channel
|
||||
- - movie
|
||||
- scene|Scene $
|
||||
- items:
|
||||
- movie
|
||||
- scene
|
||||
- title
|
||||
- ', |(|)':
|
||||
- actors
|
||||
- date|yyyy-MM-dd
|
||||
- delimit: ', '
|
||||
wrap: ['(', ')']
|
||||
items:
|
||||
- key: actors
|
||||
genders: fmtou
|
||||
- key: date
|
||||
format: yyyy-MM-dd
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
<template>
|
||||
<Dialog title="Edit summary template">
|
||||
<div class="dialog-body">
|
||||
<textarea
|
||||
v-model="template"
|
||||
height="3"
|
||||
class="input edit"
|
||||
@input="update"
|
||||
/>
|
||||
|
||||
<textarea
|
||||
:value="summary"
|
||||
class="input summary"
|
||||
:class="{ error: hasError }"
|
||||
wrap="soft"
|
||||
@click="$event.target.select()"
|
||||
/>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<div class="actions">
|
||||
<button
|
||||
class="button"
|
||||
@click="copy"
|
||||
>Copy</button>
|
||||
|
||||
<button
|
||||
class="button"
|
||||
@click="reset"
|
||||
>Reset</button>
|
||||
</div>
|
||||
|
||||
<div class="actions save">
|
||||
<!--
|
||||
<input
|
||||
class="input"
|
||||
placeholder="Name"
|
||||
>
|
||||
-->
|
||||
|
||||
<button
|
||||
class="button"
|
||||
@click="save"
|
||||
>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { parse } from 'yaml';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
import events from '#/src/events.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 cookies = Cookies.withConverter({
|
||||
write: (value) => value,
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
release: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const storedTemplate = cookies.get('summary');
|
||||
const template = ref(storedTemplate ? JSON.parse(storedTemplate)?.custom : defaultTemplate);
|
||||
const hasError = ref(false);
|
||||
|
||||
function getSummary() {
|
||||
return processSummaryTemplate(props.release, parse(template.value));
|
||||
}
|
||||
|
||||
const summary = ref(getSummary());
|
||||
|
||||
function update() {
|
||||
hasError.value = false;
|
||||
|
||||
try {
|
||||
summary.value = getSummary();
|
||||
} catch (error) {
|
||||
hasError.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
try {
|
||||
parse(template.value);
|
||||
|
||||
cookies.set('summary', JSON.stringify({ custom: template.value }), { expires: 400 }); // 100 years from now
|
||||
|
||||
events.emit('feedback', {
|
||||
type: 'success',
|
||||
message: 'Saved summary template',
|
||||
});
|
||||
} catch (error) {
|
||||
events.emit('feedback', {
|
||||
type: 'error',
|
||||
message: `Failed to save summary template: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function copy() {
|
||||
navigator.clipboard.writeText(summary.value);
|
||||
|
||||
events.emit('feedback', {
|
||||
type: 'success',
|
||||
message: 'Summary copied to clipboard',
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
template.value = defaultTemplate;
|
||||
|
||||
update();
|
||||
|
||||
events.emit('feedback', {
|
||||
type: 'undo',
|
||||
message: 'Reset summary template',
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.dialog-container .dialog) {
|
||||
background: red;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
display: flex;
|
||||
width: 50rem;
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.input {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.edit {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.summary {
|
||||
min-height: 4rem;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.5;
|
||||
|
||||
&.error {
|
||||
background: var(--background-error);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
|
||||
.input {
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
max-width: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
&.save {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -314,6 +314,13 @@
|
|||
@focus="$event.target.select()"
|
||||
>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'Edit template'"
|
||||
icon="pencil5"
|
||||
class="edit"
|
||||
@click="showSummaryDialog = true"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-tooltip="'Copy to clipboard'"
|
||||
icon="copy"
|
||||
|
@ -324,6 +331,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditSummary
|
||||
v-if="showSummaryDialog"
|
||||
:release="scene"
|
||||
@close="showSummaryDialog = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -331,7 +344,9 @@
|
|||
import { ref, computed, inject } from 'vue';
|
||||
|
||||
import { formatDate, formatDuration } from '#/utils/format.js';
|
||||
import events from '#/src/events.js';
|
||||
import getPath from '#/src/get-path.js';
|
||||
import processSummaryTemplate from '#/utils/process-summary-template.js';
|
||||
|
||||
import ActorTile from '#/components/actors/tile.vue';
|
||||
import MovieTile from '#/components/movies/tile.vue';
|
||||
|
@ -339,8 +354,7 @@ import SerieTile from '#/components/series/tile.vue';
|
|||
import Player from '#/components/video/player.vue';
|
||||
import Heart from '#/components/stashes/heart.vue';
|
||||
import Campaign from '#/components/campaigns/campaign.vue';
|
||||
|
||||
import summaryTemplate from '#/assets/summary.yaml';
|
||||
import EditSummary from '#/components/scenes/edit-summary.vue';
|
||||
|
||||
const { pageProps, campaigns } = inject('pageContext');
|
||||
const { scene } = pageProps;
|
||||
|
@ -348,6 +362,8 @@ const { scene } = pageProps;
|
|||
const playing = ref(false);
|
||||
const paused = ref(false);
|
||||
|
||||
const showSummaryDialog = ref(false);
|
||||
|
||||
const qualities = {
|
||||
2160: '4K',
|
||||
1440: 'Quad HD',
|
||||
|
@ -375,62 +391,24 @@ const poster = computed(() => {
|
|||
return null;
|
||||
});
|
||||
|
||||
const propProcessors = {
|
||||
channel: (sceneInfo) => sceneInfo.channel?.name || sceneInfo.network?.name,
|
||||
network: (sceneInfo) => sceneInfo.network?.name || sceneInfo.channel?.name,
|
||||
actors: (sceneInfo) => sceneInfo.actors.map((actor) => actor.name),
|
||||
movie: (sceneInfo) => sceneInfo.movies[0]?.title,
|
||||
date: (sceneInfo, format) => formatDate(sceneInfo.effectiveDate, format),
|
||||
};
|
||||
|
||||
function processTemplate(chain, delimit = ' ', wrapOpen = '', wrapClose = '') {
|
||||
const results = chain.reduce((result, item) => {
|
||||
if (typeof item === 'string') {
|
||||
const [prop, format] = item.split('|');
|
||||
const value = propProcessors[prop]?.(scene, format) || scene[prop];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return result.concat(value.join(format || ', '));
|
||||
}
|
||||
|
||||
return result.concat(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(item)) {
|
||||
const values = processTemplate(item, ', ');
|
||||
|
||||
return result.concat(values);
|
||||
}
|
||||
|
||||
if (typeof item === 'object') {
|
||||
const [meta, items] = Object.entries(item)[0];
|
||||
const [delimiter, wrapStart, wrapEnd] = meta.split('|');
|
||||
const values = processTemplate(items, delimiter, wrapStart, wrapEnd);
|
||||
|
||||
return result.concat(values);
|
||||
}
|
||||
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
if (results.length > 0) {
|
||||
return `${wrapOpen}${results.filter(Boolean).join(delimit)}${wrapClose}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
const summary = (() => {
|
||||
try {
|
||||
return processTemplate(summaryTemplate);
|
||||
const result = processSummaryTemplate(scene);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Failed to process template: ${error.message}`);
|
||||
console.error(`Failed to process summary template: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
function copySummary() {
|
||||
navigator.clipboard.writeText(summary);
|
||||
|
||||
events.emit('feedback', {
|
||||
type: 'success',
|
||||
message: 'Summary copied to clipboard',
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ function curateScene(rawScene, assets) {
|
|||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
priority: tag.priority,
|
||||
})),
|
||||
qualities: rawScene.qualities?.sort((qualityA, qualityB) => qualityB - qualityA) || [],
|
||||
movies: assets.movies.map((movie) => ({
|
||||
|
@ -176,7 +177,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||
tags: knex('releases_tags')
|
||||
.select('id', 'slug', 'name', 'release_id')
|
||||
.select('id', 'slug', 'name', 'priority', 'release_id')
|
||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||
.whereNotNull('tags.id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
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;
|
||||
|
||||
const genderMap = {
|
||||
f: 'female',
|
||||
m: 'male',
|
||||
t: 'transsexual',
|
||||
o: 'other',
|
||||
u: null,
|
||||
};
|
||||
|
||||
const propProcessors = {
|
||||
channel: (sceneInfo) => sceneInfo.channel?.name || sceneInfo.network?.name,
|
||||
network: (sceneInfo) => sceneInfo.network?.name || sceneInfo.channel?.name,
|
||||
actors: (sceneInfo, options) => {
|
||||
const genders = (options.genders || 'fmtou').split('').map((genderKey) => genderMap[genderKey]);
|
||||
|
||||
return sceneInfo.actors
|
||||
.filter((actor) => genders.includes(actor.gender))
|
||||
.map((actor) => actor.name);
|
||||
},
|
||||
tags: (sceneInfo, options) => sceneInfo.tags
|
||||
.filter((tag) => {
|
||||
if (options.include && !options.include.includes(tag.name) && !options.include.includes(tag.slug)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.exclude?.includes(tag.name) || options.exclude?.includes(tag.slug)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.priority && tag.priority < options.priority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((tag) => tag.name),
|
||||
movie: (sceneInfo) => sceneInfo.movies[0]?.title,
|
||||
date: (sceneInfo, options) => format(sceneInfo.effectiveDate, options.format || 'yyyy-MM-dd'),
|
||||
};
|
||||
|
||||
export default function processReleaseTemplate(release, chain = template, delimit = ' ', wrapOpen = '', wrapClose = '') {
|
||||
const results = chain.reduce((result, item) => {
|
||||
const key = typeof item === 'string' ? item : item.key;
|
||||
|
||||
if (key) {
|
||||
const value = propProcessors[key]?.(release, typeof item === 'string' ? { key } : item) || release[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return result.concat(value.join(item.delimit || ', '));
|
||||
}
|
||||
|
||||
return result.concat(item.slugify ? slugify(value, item.slugify) : value);
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
const value = processReleaseTemplate(release, item.items, item.delimit, item.wrap?.[0] || '', item.wrap?.[1] || '');
|
||||
|
||||
return result.concat(item.slugify ? slugify(value, item.slugify) : value);
|
||||
}
|
||||
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
if (results.length > 0) {
|
||||
return `${wrapOpen}${results.filter(Boolean).join(delimit)}${wrapClose}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
Loading…
Reference in New Issue