Added summary template editor, improved summary template format and options.

This commit is contained in:
DebaucheryLibrarian 2024-08-25 02:51:16 +02:00
parent d2c9b447ee
commit e2cffbdde2
6 changed files with 313 additions and 56 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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