media detail

This commit is contained in:
2021-12-19 01:16:02 +01:00
parent a4a6c8a1d7
commit facb41910b
18 changed files with 367 additions and 42 deletions
+3 -1
View File
@@ -26,6 +26,7 @@ import uiTrinity from './ui-trinity.vue';
import uiPick from './ui-pick.vue';
import uiDatagrid from './ui-datagrid.vue';
import uiProgress from './ui-progress.vue';
import uiDate from './ui-date.vue';
export {
uiIcon,
@@ -55,5 +56,6 @@ export {
uiTrinity,
uiPick,
uiDatagrid,
uiProgress
uiProgress,
uiDate
};
@@ -4,8 +4,7 @@
<script>
import dayjs from 'dayjs';
import Strings from 'zero/helpers/strings.js';
import { formatDate } from '../utils/dates';
export default {
name: 'uiDate',
@@ -53,11 +52,11 @@
if (!this.split)
{
this.output = Strings.date(this.value, this.format);
this.output = formatDate(this.value, this.format);
}
else
{
this.output = Strings.date(this.value, 'short') + ' <span class="-minor">' + Strings.date(this.value, 'time') + '</span>';
this.output = formatDate(this.value, 'short') + ' <span class="-minor">' + formatDate(this.value, 'time') + '</span>';
}
}
@@ -1,5 +1,5 @@
<template>
<ui-property v-if="!isHidden"
-<template>
<ui-property v-if="loaded && !isHidden"
:field="config.path"
:label="label"
:hide-label="config.options.hideLabel"
@@ -119,7 +119,7 @@
{
for (var key of this.selector)
{
if (key in currentValue)
if (currentValue && key in currentValue)
{
found = true;
currentValue = currentValue[key];
@@ -132,8 +132,6 @@
this.model = found ? currentValue : null;
}
//console.info(this.config.path + ' => ' + this.model);
},
+1 -1
View File
@@ -5,7 +5,7 @@
</header>
<div class="editor" :class="['display-' + display, { 'has-sidebar': asideDefined, 'hide-tabs': tabs.length < 2, 'has-below': belowDefined }]">
<ui-tabs class="editor-tabs">
<ui-tab v-for="(tab, index) in tabs" class="ui-box" :class="tab.class" :label="tab.name" :count="tab.count(value)" :hidden="tab.disabled(value)" :key="index">
<ui-tab v-for="(tab, index) in tabs" class="ui-box" :class="tab.class" :data-alias="tab.alias" :label="tab.name" :count="tab.count(value)" :hidden="tab.disabled(value)" :key="index">
<h3 v-if="display == 'boxes' && tab.name" class="ui-headline editor-tab-headline" v-localize="tab.name"></h3>
<slot name="blueprint">
<!--<blueprint-property v-if="value && editorConfig.blueprint" :model-value="value" :meta="meta" :config="editorConfig.blueprint" />-->
@@ -5,7 +5,7 @@
<script>
export default {
props: {
value: [Object, String, Array, Number, Boolean],
value: Object,
entity: Object,
render: Function
},
@@ -19,8 +19,6 @@ const files = {
delete: (id: string, config?: ApiRequestConfig) => del('media/' + id, config),
//uploadtest: (model: any, config?: ApiRequestConfig) => post('media/uploadtest', model, config),
upload: async (file: File, folderId?: string, onProgress?: any) =>
{
var data = new FormData();
@@ -1,12 +1,81 @@
<template>
<div>
media {{id}}
</div>
<ui-form ref="form" class="media-detail" v-slot="form" @submit="onSubmit" @load="onLoad" :route="route">
<ui-form-header v-model:value="model" prefix="@media.list" title="@media.name" :disabled="disabled" :active-disabled="true" :is-create="!id" :state="form.state" :can-delete="meta.canDelete" @delete="onDelete">
<template v-slot:actions>
<ui-dropdown-button label="Open" icon="fth-external-link" />
<ui-dropdown-button label="Replace file" icon="fth-file-input" :disabled="disabled" />
</template>
</ui-form-header>
<div class="media-detail-grid">
<media-detail-preview :model="model" />
<ui-editor config="media:edit" v-model="model" :meta="meta" :disabled="disabled" />
</div>
</ui-form>
</template>
<script>
import api from '../../api';
import MediaDetailPreview from './preview.vue';
export default {
props: ['id']
props: ['id'],
data: () => ({
meta: {},
model: { name: null },
route: 'media-edit',
disabled: false
}),
components: { MediaDetailPreview },
methods: {
async onLoad(form)
{
const response = await form.load(() => this.id ? api.getById(this.id) : api.getEmpty(this.$route.query['zero.flavor']));
this.model = response;
},
async onSubmit(form)
{
const response = this.id ? await api.update(this.model) : await api.create(this.model);
await form.handle(response);
},
onDelete(item, opts)
{
opts.hide();
this.$refs.form.onDelete(api.delete.bind(this, this.id));
}
}
}
</script>
</script>
<style lang="scss">
.media-detail-grid
{
display: grid;
grid-template-columns: minmax(auto, 1fr) 420px;
grid-gap: var(--padding-xs);
max-width: 1160px;
margin: 0 auto;
padding: 0 var(--padding);
.editor
{
padding: 0;
}
}
.media-detail-center
{
display: grid;
grid-template-rows: auto minmax(auto, 1fr);
grid-gap: var(--padding-xs);
}
</style>
@@ -0,0 +1,58 @@
<template>
<div class="media-detail-metadata">
<ui-property label="@media.fields.size" :vertical="false">
<span v-filesize="entity.size"></span>
</ui-property>
<ui-property label="@ui.createdDate" field="createdDate" :vertical="false">
<ui-date :value="entity.createdDate" />
</ui-property>
<ui-property v-if="entity.lastModifiedDate" :vertical="false" label="@ui.modifiedDate">
<ui-date :value="entity.lastModifiedDate" />
</ui-property>
<template v-if="entity.imageMeta">
<ui-property v-if="entity.imageMeta.width" label="@media.fields.imageMeta.dimension" :is-text="true" :vertical="false">
{{entity.imageMeta.width}} × {{entity.imageMeta.height}}
</ui-property>
<ui-property v-if="entity.imageMeta.dpi != 0" label="@media.fields.imageMeta.dpi" :is-text="true" :vertical="false">
{{entity.imageMeta.dpi}}
</ui-property>
<ui-property v-if="entity.imageMeta.colorSpace" label="@media.fields.imageMeta.colorSpace" :is-text="true" :vertical="false">
{{entity.imageMeta.colorSpace}}
</ui-property>
<ui-property v-if="entity.imageMeta.frames > 1" label="@media.fields.imageMeta.frames" :is-text="true" :vertical="false">
{{entity.imageMeta.frames}}
</ui-property>
</template>
</div>
</template>
<script>
export default {
props: {
value: Object,
entity: Object
}
}
</script>
<style lang="scss">
.media-detail
{
.ui-property + .ui-property
{
margin-top: 15px;
padding-top: 15px;
border-top: none;
}
}
.media-detail-metadata
{
.ui-property + .ui-property
{
margin-top: var(--padding-xs);
padding-top: 0;
border-top: none;
}
}
</style>
@@ -0,0 +1,148 @@
<template>
<div class="media-detail-file media-pattern">
<div v-if="model.imageMeta" ref="image" class="media-detail-file-image" @click="setFocalPoint" @dblclick="model.imageMeta.focalPoint = null">
<ui-thumbnail v-if="model.id" :media="model.id" size="preview" />
<span class="media-detail-file-focal-point" :style="getFocalPointStyle(model.imageMeta.focalPoint)"></span>
</div>
<div v-else class="media-detail-file-generic">
<ui-icon symbol="fth-file-text" :size="64" :stroke="1" />
<p>
<span v-localize="extension"></span><br /><span v-filesize="model.size"></span>
</p>
</div>
</div>
</template>
<script lang="ts">
//import * as overlays from '../../../../services/overlay';
export default {
name: 'mediaDetailPreview',
props: {
model: Object,
disabled: {
type: Boolean,
default: false
}
},
data: () => ({
}),
computed: {
extension()
{
if (!this.model || !this.model.path)
{
return '@media.untitledName';
}
const parts = this.model.path.split('.');
return parts[parts.length - 1].toUpperCase();
}
},
methods: {
getFocalPointStyle(point)
{
return {
display: 'inline-block',
left: (point ? point.left : 0.5) * 100 + '%',
top: (point ? point.top : 0.5) * 100 + '%'
}
},
setFocalPoint(ev)
{
if (this.disabled)
{
return;
}
let image = this.$refs.image.getBoundingClientRect();
let point = { x: ev.pageX - image.x, y: ev.pageY - image.y };
const left = +(point.x / image.width).toFixed(2);
const top = +(point.y / image.height).toFixed(2);
this.model.imageMeta.focalPoint = {
left: left < 0 ? 0 : (left > 1 ? 1 : left),
top: top < 0 ? 0 : (top > 1 ? 1 : top)
};
}
}
};
</script>
<style lang="scss">
.media-detail-file
{
display: flex;
justify-content: center;
align-items: center;
padding: var(--padding);
border-radius: var(--radius);
img
{
border-radius: var(--radius);
max-height: 550px;
z-index: 1;
}
.ui-icon
{
z-index: 1;
color: var(--color-text);
}
}
.media-detail-file-image
{
display: inline-block;
border-radius: var(--radius);
position: relative;
}
.media-detail-file-generic
{
position: relative;
z-index: 1;
text-align: center;
p
{
margin-top: 0.8em;
font-size: var(--font-size-s);
color: var(--color-text-dim);
line-height: 1.5;
&:first-line
{
font-weight: 700;
font-size: var(--font-size);
color: var(--color-text);
}
}
}
.media-detail-file-focal-point
{
display: none;
position: absolute;
left: 50%;
top: 50%;
margin: -8px 0 0 -8px;
width: 16px;
height: 16px;
border-radius: 20px;
background: white;
border: 3px solid #222;
box-shadow: 1px 1px 2px rgba(0,0,0,0.4);
z-index: 2;
}
</style>
@@ -1,18 +1,19 @@
<template>
<router-link :to="link" class="media-item" :class="{ 'is-selected': selected}">
<div class="media-item-preview" :class="{'media-pattern': value.image, 'is-covered': covered }">
<div class="media-item" :class="{ 'is-selected': selected}">
<router-link :to="link" class="media-item-preview" :class="{'media-pattern': !value.isFolder, 'is-covered': covered }">
<span class="media-item-check"><ui-icon symbol="fth-check" :size="14" /></span>
<img class="media-item-image" v-if="value.image" :src="value.image" />
<span class="media-item-icon" v-if="!value.image"><ui-icon :symbol="(value.isFolder ? 'fth-folder' : 'fth-file')" :size="26" :stroke-width="2" /></span>
</div>
</router-link>
<p v-if="value.isFolder" class="media-item-text" :title="value.name">
<!--<input type="text" v-model="value.name" />-->
{{value.name}} <!--<ui-icon symbol="fth-cloud" v-if="value.isShared" :size="15" class="media-item-shared" />-->
<span class="-minor"> ({{value.children}})<!--<span v-localize="{ key: value.children === 1 ? '@media.child_count_1' : '@media.child_count_x', tokens: { count: value.children }}"></span>--></span>
</p>
<p v-else class="media-item-text -minor">
<span v-filesize:0="value.size"></span> | <span :title="value.name">{{value.name}}</span>
<span v-filesize:0="value.size"></span> | <span :title="value.name">{{value.name}}</span>
</p>
</router-link>
</div>
</template>
<script>
@@ -88,6 +89,7 @@
text-align: center;
box-shadow: var(--shadow-short);
border: 2px solid transparent;
color: var(--color-text);
}
.media-item.is-selected .media-item-preview
@@ -105,6 +107,11 @@
z-index: 1;
}
.media-item-icon
{
z-index: 1;
}
.media-item-preview.is-covered .media-item-image
{
object-fit: cover;
@@ -156,4 +163,13 @@
position: relative;
top: 2px;
}
.media-item input[type="text"]
{
border: none;
height: auto;
padding: 0;
background: none;
width: auto;
}
</style>
@@ -31,7 +31,7 @@
<template v-if="selected.length < 1" v-slot:actions="props">
<template v-if="props.item && props.item.isFolder">
<ui-dropdown-button label="@ui.edit.title" icon="fth-edit-2" @click="editFolder(props.item)" />
<ui-dropdown-button label="@ui.rename.title" icon="fth-edit-2" @click="editFolder(props.item)" />
<ui-dropdown-button label="@ui.move.title" icon="fth-corner-down-right" @click="move([props.item])" />
<ui-dropdown-button label="@ui.selection.select" icon="fth-check-circle-2" @click="$refs.grid.select(props.item)" />
<ui-dropdown-separator />
@@ -12,6 +12,6 @@ export default {
app.route({ name: 'media-edit', path: '/media/edit/:id?', component: () => import('./pages/detail/detail.vue'), props: true });
//app.schema('countries', () => import('./schemas/list'));
//app.schema('countries:edit', () => import('./schemas/editor'));
app.schema('media:edit', () => import('./schemas/editor'));
}
} as ZeroPlugin;
@@ -1,12 +1,40 @@
import Editor from '../../../editor/editor';
import { getFilesize, formatDate } from '../../../utils';
import MetadataEditor from '../pages/detail/metadata.vue';
const editor = new Editor('countries:edit', '@country.fields.');
editor.blueprintAlias = 'country';
const editor = new Editor('media:edit', '@media.fields.');
editor.blueprintAlias = 'media';
editor.field('name', { label: '@ui.name' }).text(120).required();
//editor.field('alias', { label: '@ui.alias' }).text().required();
editor.field('code').text(2).required();
editor.field('isPreferred').toggle();
editor.field('name', { label: '@ui.name' }).text().required();
//editor.field('source').custom(MediaUpload).required();
editor.field('imageMeta.alternativeText').when(x => x.imageMeta).text();
editor.field('caption').textarea();
editor.field('createdDate', { hideLabel: true }).custom(MetadataEditor);
//editor.field('size').vertical(false).output(x => getFilesize(x, 2));
//editor.field('createdDate', { label: '@ui.createdDate' }).vertical(false).output(x => formatDate(x));
//editor.field('lastModifiedDate', { label: '@ui.modifiedDate' }).vertical(false).output(x => formatDate(x));
//editor.field('imageMeta.width').vertical(false).output((x, entity) => entity.imageMeta.width + ' × ' + entity.imageMeta.height);
////meta.field('imageMeta.imageTakenDate').vertical(false).output(x => formatDate(x));
////meta.field('imageMeta.frames').vertical(false).output();
//editor.field('imageMeta.dpi').vertical(false).output();
//<ui-property v-if="modelValue.lastModifiedDate" field="lastModifiedDate" label="@ui.modifiedDate">
// <ui-date v-model="modelValue.lastModifiedDate" />
// </ui-property>
// <ui-property label="@ui.createdDate" field="createdDate">
// <ui-date v-model="modelValue.createdDate" />
// </ui-property>
// <ui-property label="@ui.entityfields.alias" field="alias">
// {{modelValue.alias}}
// </ui-property>
// <ui-property label="@ui.entityfields.sort" field="sort">
// {{modelValue.sort}}
// </ui-property>
// <ui-property v-if="modelValue.key" label="@ui.entityfields.key" field="key">
// {{modelValue.key}}
// </ui-property>
export default editor;
@@ -23,6 +23,11 @@ export const localize = (key: string, options?: LocalizeOptions): string =>
return '';
}
if (!isNaN(key))
{
return key;
}
const hasAtSign = key.indexOf('@') === 0;
if (!params.force && !hasAtSign)
@@ -3,7 +3,7 @@
// theme-agnostic colors
:root
{
--color-accent: #67bdb1; // #F9AA19 // #67bdb1
--color-accent: #f9aa19; // #F9AA19 // #67bdb1
--color-accent-fg: #ffffff;
--color-negative: rgb(216, 40, 83);
@@ -642,6 +642,7 @@
"name": "Media",
"addfolder": "Add folder",
"editfolder": "Edit folder",
"untitledName": "[untitled]",
"actions": {
"folderdropdown": "Folder",
"addfolder": "Create folder",
@@ -658,17 +659,20 @@
"foldername_placeholder": "Enter a name ...",
"caption": "Caption",
"caption_text": "Additional caption text",
"dimension": "Dimension",
"size": "Size",
"date": "Last modified at",
"filename": "Filename",
"source": "File",
"alternativeText": "Alternative text",
"alternativeText_text": "Text which is used when the image can't be loaded",
"dpi": "DPI",
"colorSpace": "Color space",
"hasTransparency": "Has transparency",
"frames": "Frames"
"imageMeta": {
"alternativeText": "Alternative text",
"alternativeText_text": "Text which is used when the image can't be loaded",
"dpi": "DPI",
"colorSpace": "Color space",
"hasTransparency": "Has transparency",
"frames": "Frames",
"imageTakenDate": "Date taken",
"dimension": "Dimension"
}
},
"deleteoverlay": {
"title": "Delete media",
+1 -1
View File
@@ -33,7 +33,7 @@ public class MediaCreator : IMediaCreator
string normalizedFilename = Safenames.File(filename);
bool isImage = Options.AllowedImageFileExtensions.Contains(fileExtension, StringComparer.InvariantCultureIgnoreCase);
bool isDocument = !isImage && Options.AllowedOtherFileExtensions.Contains(filename, StringComparer.InvariantCultureIgnoreCase);
bool isDocument = !isImage && Options.AllowedOtherFileExtensions.Contains(fileExtension, StringComparer.InvariantCultureIgnoreCase);
if (!isImage && !isDocument)
{
+1 -1
View File
@@ -54,7 +54,7 @@ public class ZeroDocumentStore : DocumentStore, IZeroDocumentStore
};
session.Advanced.WaitForIndexesAfterSaveChanges();
//session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromHours(1), options.Database); // TODO I guess this will not work in backoffice when we want to see changes immediately
session.Advanced.DocumentStore.AggressivelyCacheFor(TimeSpan.FromHours(1), options.Database); // TODO I guess this will not work in backoffice when we want to see changes immediately
// maybe use caching for frontend but not for backoffice?
return session;