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-20: #eee;
|
||||||
--background-level-30: #eee;
|
--background-level-30: #eee;
|
||||||
--background-dim: var(--shadow-weak-10);
|
--background-dim: var(--shadow-weak-10);
|
||||||
|
--background-error: rgba(255, 0, 0, .1);
|
||||||
|
|
||||||
--shadow-weak-50: rgba(0, 0, 0, .02);
|
--shadow-weak-50: rgba(0, 0, 0, .02);
|
||||||
--shadow-weak-40: rgba(0, 0, 0, .05);
|
--shadow-weak-40: rgba(0, 0, 0, .05);
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
- ' - ':
|
- delimit: ' - '
|
||||||
|
items:
|
||||||
- channel
|
- channel
|
||||||
- - movie
|
- items:
|
||||||
- scene|Scene $
|
- movie
|
||||||
|
- scene
|
||||||
- title
|
- title
|
||||||
- ', |(|)':
|
- delimit: ', '
|
||||||
- actors
|
wrap: ['(', ')']
|
||||||
- date|yyyy-MM-dd
|
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()"
|
@focus="$event.target.select()"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-tooltip="'Edit template'"
|
||||||
|
icon="pencil5"
|
||||||
|
class="edit"
|
||||||
|
@click="showSummaryDialog = true"
|
||||||
|
/>
|
||||||
|
|
||||||
<Icon
|
<Icon
|
||||||
v-tooltip="'Copy to clipboard'"
|
v-tooltip="'Copy to clipboard'"
|
||||||
icon="copy"
|
icon="copy"
|
||||||
|
@ -324,6 +331,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<EditSummary
|
||||||
|
v-if="showSummaryDialog"
|
||||||
|
:release="scene"
|
||||||
|
@close="showSummaryDialog = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -331,7 +344,9 @@
|
||||||
import { ref, computed, inject } from 'vue';
|
import { ref, computed, inject } from 'vue';
|
||||||
|
|
||||||
import { formatDate, formatDuration } from '#/utils/format.js';
|
import { formatDate, formatDuration } from '#/utils/format.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
import getPath from '#/src/get-path.js';
|
import getPath from '#/src/get-path.js';
|
||||||
|
import processSummaryTemplate from '#/utils/process-summary-template.js';
|
||||||
|
|
||||||
import ActorTile from '#/components/actors/tile.vue';
|
import ActorTile from '#/components/actors/tile.vue';
|
||||||
import MovieTile from '#/components/movies/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 Player from '#/components/video/player.vue';
|
||||||
import Heart from '#/components/stashes/heart.vue';
|
import Heart from '#/components/stashes/heart.vue';
|
||||||
import Campaign from '#/components/campaigns/campaign.vue';
|
import Campaign from '#/components/campaigns/campaign.vue';
|
||||||
|
import EditSummary from '#/components/scenes/edit-summary.vue';
|
||||||
import summaryTemplate from '#/assets/summary.yaml';
|
|
||||||
|
|
||||||
const { pageProps, campaigns } = inject('pageContext');
|
const { pageProps, campaigns } = inject('pageContext');
|
||||||
const { scene } = pageProps;
|
const { scene } = pageProps;
|
||||||
|
@ -348,6 +362,8 @@ const { scene } = pageProps;
|
||||||
const playing = ref(false);
|
const playing = ref(false);
|
||||||
const paused = ref(false);
|
const paused = ref(false);
|
||||||
|
|
||||||
|
const showSummaryDialog = ref(false);
|
||||||
|
|
||||||
const qualities = {
|
const qualities = {
|
||||||
2160: '4K',
|
2160: '4K',
|
||||||
1440: 'Quad HD',
|
1440: 'Quad HD',
|
||||||
|
@ -375,62 +391,24 @@ const poster = computed(() => {
|
||||||
return null;
|
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 = (() => {
|
const summary = (() => {
|
||||||
try {
|
try {
|
||||||
return processTemplate(summaryTemplate);
|
const result = processSummaryTemplate(scene);
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to process template: ${error.message}`);
|
console.error(`Failed to process summary template: ${error.message}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
function copySummary() {
|
function copySummary() {
|
||||||
navigator.clipboard.writeText(summary);
|
navigator.clipboard.writeText(summary);
|
||||||
|
|
||||||
|
events.emit('feedback', {
|
||||||
|
type: 'success',
|
||||||
|
message: 'Summary copied to clipboard',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,7 @@ function curateScene(rawScene, assets) {
|
||||||
id: tag.id,
|
id: tag.id,
|
||||||
slug: tag.slug,
|
slug: tag.slug,
|
||||||
name: tag.name,
|
name: tag.name,
|
||||||
|
priority: tag.priority,
|
||||||
})),
|
})),
|
||||||
qualities: rawScene.qualities?.sort((qualityA, qualityB) => qualityB - qualityA) || [],
|
qualities: rawScene.qualities?.sort((qualityA, qualityB) => qualityB - qualityA) || [],
|
||||||
movies: assets.movies.map((movie) => ({
|
movies: assets.movies.map((movie) => ({
|
||||||
|
@ -176,7 +177,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||||
tags: knex('releases_tags')
|
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')
|
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||||
.whereNotNull('tags.id')
|
.whereNotNull('tags.id')
|
||||||
.whereIn('release_id', sceneIds)
|
.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