start media upload
This commit is contained in:
@@ -198,19 +198,21 @@ public abstract class ZeroApiEntityStoreController<TModel, TStore> : ZeroApiCont
|
||||
return BadRequest(Result.Fail(nameof(id), "@errors.onupdate.noidmatch"));
|
||||
}
|
||||
|
||||
TModel model = await Store.Load(id);
|
||||
// TODO throws error on save: Attempted to associate a different object with id ...
|
||||
// we need to map props to new object
|
||||
//TModel model = await Store.Load(id);
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
return BadRequest(Result.Fail(nameof(id), "@errors.idnotfound"));
|
||||
}
|
||||
//if (model == null)
|
||||
//{
|
||||
// return BadRequest(Result.Fail(nameof(id), "@errors.idnotfound"));
|
||||
//}
|
||||
|
||||
string storedChangeToken = Store.GetChangeToken(model);
|
||||
//string storedChangeToken = Store.GetChangeToken(model);
|
||||
|
||||
if (!changeToken.IsNullOrEmpty() && storedChangeToken != changeToken)
|
||||
{
|
||||
return BadRequest(Result.Fail("@errors.onupdate.changetokenmismatch"));
|
||||
}
|
||||
//if (!changeToken.IsNullOrEmpty() && storedChangeToken != changeToken)
|
||||
//{
|
||||
// return BadRequest(Result.Fail("@errors.onupdate.changetokenmismatch"));
|
||||
//}
|
||||
|
||||
Result<TModel> result = await Store.Update(updateModel);
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ public class MediaMapperProfile : ZeroMapperProfile
|
||||
target.Children = 0;
|
||||
target.Size = source.Size;
|
||||
|
||||
if (source.Thumbnails.Any())
|
||||
if (source.ImageMeta != null && source.ImageMeta.Thumbnails.Any())
|
||||
{
|
||||
string path = source.Thumbnails.GetValueOrDefault("preview");
|
||||
string path = source.ImageMeta.Thumbnails.GetValueOrDefault("preview");
|
||||
target.Image = path.IsNullOrEmpty() ? null : FileSystem.MapToPublicPath(path);
|
||||
}
|
||||
}
|
||||
@@ -54,13 +54,13 @@ public class MediaMapperProfile : ZeroMapperProfile
|
||||
protected virtual void Map(zero.Media.Media source, MediaFileEdit target, IZeroMapperContext ctx)
|
||||
{
|
||||
Map(source, (MediaEdit)target, ctx);
|
||||
target.AlternativeText = source.AlternativeText;
|
||||
target.AlternativeText = source.ImageMeta?.AlternativeText;
|
||||
target.Caption = source.Caption;
|
||||
target.Path = source.Path;
|
||||
target.Thumbnails = source.Thumbnails;
|
||||
target.Thumbnails = source.ImageMeta?.Thumbnails;
|
||||
target.Size = source.Size;
|
||||
target.ImageMeta = source.ImageMeta;
|
||||
target.FocalPoint = source.FocalPoint;
|
||||
target.FocalPoint = source.ImageMeta?.FocalPoint;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Raven.Client.Documents;
|
||||
@@ -40,6 +41,13 @@ public class MediaController : ZeroApiTreeEntityStoreController<zero.Media.Media
|
||||
Media = media;
|
||||
}
|
||||
|
||||
[HttpPost("uploadtest")]
|
||||
public async Task<ActionResult> Upload(IFormFile file)
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
#region bulk operations
|
||||
|
||||
[HttpPut("bulk/move")]
|
||||
|
||||
@@ -17,7 +17,27 @@ const files = {
|
||||
|
||||
move: (id: string, parentId: string, config?: ApiRequestConfig) => put(`media/${id}/move/${parentId}`, {}, { ...config }),
|
||||
|
||||
delete: (id: string, config?: ApiRequestConfig) => del('media/' + id, config)
|
||||
delete: (id: string, config?: ApiRequestConfig) => del('media/' + id, config),
|
||||
|
||||
//uploadtest: (model: any, config?: ApiRequestConfig) => post('media/uploadtest', model, config),
|
||||
|
||||
uploadtest: async (file, onProgress) =>
|
||||
{
|
||||
var data = new FormData();
|
||||
data.append('file', file);
|
||||
//data.append('folderId', folderId);
|
||||
|
||||
return await post('media/uploadtest', data, {
|
||||
onUploadProgress: (progressEvent) =>
|
||||
{
|
||||
if (typeof onProgress === 'function')
|
||||
{
|
||||
var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
onProgress(percentCompleted);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
+1
-34
@@ -8,11 +8,9 @@
|
||||
<ui-error :catch-all="true" />
|
||||
</ui-property>
|
||||
</div>
|
||||
<br /><br />
|
||||
<div class="app-confirm-buttons">
|
||||
<ui-button type="accent" v-if="!disabled" :submit="true" :state="form.state" :label="model.id ? '@ui.save' : '@ui.create'"></ui-button>
|
||||
<ui-button type="accent" v-if="!disabled" :submit="true" :state="form.state" :label="model.id ? '@ui.update' : '@ui.create'"></ui-button>
|
||||
<ui-button type="light" label="@ui.close" :disabled="loading" @click="config.close"></ui-button>
|
||||
<ui-button v-if="!disabled && model.id" type="light" label="@ui.delete" @click="onDelete" style="float:right;"></ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</ui-form>
|
||||
@@ -58,37 +56,6 @@
|
||||
{
|
||||
this.config.confirm(response.data);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
//onDelete(item, opts)
|
||||
//{
|
||||
// opts.hide();
|
||||
// thi
|
||||
|
||||
//onLoad(form)
|
||||
//{
|
||||
// form.load(!this.model.id ? MediaFolderApi.getEmpty() : MediaFolderApi.getById(this.model.id)).then(response =>
|
||||
// {
|
||||
// this.disabled = false; //!response.canEdit;
|
||||
// this.item = response.entity;
|
||||
// this.item.parentId = this.model.parentId;
|
||||
// this.loading = false;
|
||||
// });
|
||||
//},
|
||||
|
||||
|
||||
//onSubmit(form)
|
||||
//{
|
||||
// form.handle(MediaFolderApi.save(this.item)).then(response =>
|
||||
// {
|
||||
// this.config.confirm(response, this.config);
|
||||
// });
|
||||
//},
|
||||
|
||||
onDelete()
|
||||
{
|
||||
//this.config.confirm({ deleted: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div class="ui-media-upload">
|
||||
<h2 class="ui-headline" v-localize="'Upload status'"></h2>
|
||||
|
||||
<div class="ui-media-upload-items">
|
||||
<button type="button" v-for="item in items" class="ui-media-upload-item">
|
||||
<img class="-preview" v-if="item.isImage" :src="item.source" />
|
||||
<span class="-preview" v-if="!item.isImage"><ui-icon symbol="fth-file" :size="20" /></span>
|
||||
<p class="ui-media-upload-item-text">
|
||||
{{item.name}}
|
||||
<span class="-minor">
|
||||
<br>
|
||||
<span v-if="item.progress < 100 && !item.error && !item.success" class="ui-media-upload-item-progress">
|
||||
<span class="-inner" :style="{ width: item.progress + '%' }"></span>
|
||||
</span>
|
||||
<span v-if="item.success">Completed</span>
|
||||
</span>
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="app-confirm-buttons">
|
||||
<ui-button v-if="!disabled" type="action" label="Upload" @click="upload"></ui-button>
|
||||
<ui-button type="light" label="@ui.close" :disabled="loading" @click="config.close"></ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import api from '../api';
|
||||
import { generateId } from '../../../utils/numbers';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
model: FileList,
|
||||
config: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: false,
|
||||
items: []
|
||||
}),
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
const files = this.model;
|
||||
|
||||
console.info(files);
|
||||
let items = [];
|
||||
|
||||
if (!files || files.length < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < files.length; i++)
|
||||
{
|
||||
let file = files[i];
|
||||
|
||||
this.items.push({
|
||||
id: 'upload:' + generateId(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
mimeType: file.type,
|
||||
source: null,
|
||||
progress: 0,
|
||||
file: file,
|
||||
isImage: false,
|
||||
success: false,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
upload()
|
||||
{
|
||||
for (var i = 0; i < this.items.length; i++)
|
||||
{
|
||||
let item = this.items[i];
|
||||
|
||||
api.uploadtest(item.file, progress =>
|
||||
{
|
||||
console.info({ name: item.file.name, progress });
|
||||
item.progress = progress;
|
||||
}).then(res =>
|
||||
{
|
||||
console.info('success');
|
||||
//if (res.success)
|
||||
//{
|
||||
// item.source = res.model.thumbnailSource || res.model.source;
|
||||
// item.isImage = res.model.type === 'image';
|
||||
// item.success = true;
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// item.success = false;
|
||||
// // TODO output error
|
||||
//}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-media-upload
|
||||
{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-media-upload
|
||||
{
|
||||
/*height: 200px;
|
||||
background: var(--color-bg-mid);
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
margin-top: var(--padding);
|
||||
padding: 10px;
|
||||
overflow: hidden;*/
|
||||
}
|
||||
|
||||
.ui-media-upload-items
|
||||
{
|
||||
margin-top: 25px;
|
||||
max-height: 495px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ui-media-upload-item
|
||||
{
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
display: grid;
|
||||
grid-template-columns: 70px 1fr;
|
||||
gap: 15px;
|
||||
align-items: center;
|
||||
line-height: 1.4;
|
||||
|
||||
& + .ui-media-upload-item
|
||||
{
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.-preview
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
height: 70px;
|
||||
width: 70px;
|
||||
object-fit: cover;
|
||||
background: var(--color-bg-mid);
|
||||
border-radius: var(--radius);
|
||||
position: relative;
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
}
|
||||
/*&.is-upload .-preview
|
||||
{
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}*/
|
||||
|
||||
.-extension
|
||||
{
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.-minor
|
||||
{
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-media-upload-item-progress
|
||||
{
|
||||
display: block;
|
||||
width: 50%;
|
||||
height: 8px;
|
||||
margin-top: 6px;
|
||||
border-radius: 2px;
|
||||
background: var(--color-bg-mid);
|
||||
position: relative;
|
||||
|
||||
.-inner
|
||||
{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,48 @@
|
||||
<template>
|
||||
<div class="media-overview-drop">
|
||||
<form enctype="multipart/form-data" class="media-overview-drop" :class="{ 'is-dragging': dragging }"
|
||||
@dragenter.prevent="onDragEnter" @dragleave.prevent="onDragLeave" @dragover.prevent="" @drop.prevent.stop="onDrop">
|
||||
<ui-icon symbol="fth-upload" :size="26" :stroke-width="2" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import * as overlays from '../../../../services/overlay';
|
||||
|
||||
export default {
|
||||
name: 'mediaOverviewDrop',
|
||||
|
||||
props: {
|
||||
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
dragging: false
|
||||
}),
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
async onDrop(ev)
|
||||
{
|
||||
console.info(ev.dataTransfer);
|
||||
this.dragging = false;
|
||||
|
||||
const result = await overlays.open({
|
||||
component: () => import('../../overlays/upload-status.vue'),
|
||||
model: ev.dataTransfer.files
|
||||
});
|
||||
},
|
||||
|
||||
onDragEnter(ev)
|
||||
{
|
||||
this.dragging = true;
|
||||
},
|
||||
|
||||
onDrageLeave(ev)
|
||||
{
|
||||
this.dragging = false;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -27,6 +60,11 @@
|
||||
box-shadow: var(--shadow-short);
|
||||
border: 1px dashed var(--color-line-dashed-onbg);
|
||||
color: var(--color-text);
|
||||
|
||||
&.is-dragging
|
||||
{
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-datagrid-outer.is-selecting .media-overview-drop
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<template v-if="selected.length < 1">
|
||||
<ui-search v-model="gridConfig.search" class="onbg" />
|
||||
<ui-button type="accent" label="@media.addfolder" @click="addFolder" />
|
||||
<ui-button type="accent" label="@media.addfolder" @click="editFolder()" />
|
||||
</template>
|
||||
<media-selection v-else :selected="selected" @clear="clearSelection" @move="move" @remove="remove" />
|
||||
</ui-header-bar>
|
||||
@@ -30,12 +30,20 @@
|
||||
</template>
|
||||
|
||||
<template v-if="selected.length < 1" v-slot:actions="props">
|
||||
<ui-dropdown-button v-if="props.item && props.item.isFolder" label="@ui.open.title" icon="fth-arrow-right" @click="goToFolder(props.item.id)" />
|
||||
<ui-dropdown-button label="@ui.edit.title" icon="fth-edit-2" @click="edit(props.item, props.item.isFolder)" />
|
||||
<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 />
|
||||
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove(props.item, props.item.isFolder)" />
|
||||
<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.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 />
|
||||
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove([props.item])" />
|
||||
</template>
|
||||
<template v-if="props.item && !props.item.isFolder">
|
||||
<ui-dropdown-button label="@ui.open.title" icon="fth-arrow-right" @click="goToFolder(props.item.id)" />
|
||||
<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 />
|
||||
<ui-dropdown-button label="@ui.delete" icon="fth-trash" @click="remove([props.item])" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-slot:default="props">
|
||||
@@ -149,11 +157,11 @@
|
||||
},
|
||||
|
||||
|
||||
async addFolder()
|
||||
async editFolder(item)
|
||||
{
|
||||
const result = await overlays.open({
|
||||
component: () => import('../../overlays/createfolder.vue'),
|
||||
model: { parentId: this.parentId },
|
||||
component: () => import('../../overlays/editfolder.vue'),
|
||||
model: item || { parentId: this.parentId },
|
||||
display: 'dialog'
|
||||
});
|
||||
|
||||
|
||||
@@ -204,4 +204,14 @@
|
||||
transform: scale(1) translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.app-confirm-buttons
|
||||
{
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.app-confirm p
|
||||
{
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
@@ -64,16 +64,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app-confirm-buttons
|
||||
{
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.app-confirm p
|
||||
{
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
@@ -27,9 +27,9 @@
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import MediaApi from 'zero/api/media.js'
|
||||
import Strings from 'zero/helpers/strings.js';
|
||||
<script lang="ts">
|
||||
import api from '../api';
|
||||
import { generateId } from '../../../utils/numbers';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
let file = files[i];
|
||||
|
||||
this.items.push({
|
||||
id: 'upload:' + Strings.guid(),
|
||||
id: 'upload:' + generateId(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
mimeType: file.type,
|
||||
@@ -77,7 +77,7 @@
|
||||
{
|
||||
let item = this.items[i];
|
||||
|
||||
MediaApi.upload(item.file, this.config.folderId, progress =>
|
||||
api.uploadtest(item.file, this.config.folderId, progress =>
|
||||
{
|
||||
item.progress = progress;
|
||||
}).then(res =>
|
||||
|
||||
@@ -154,7 +154,7 @@ public class ZeroVue : IZeroVue
|
||||
{
|
||||
Id = app.Id,
|
||||
Name = app.Name,
|
||||
Image = app.IconId.IsNullOrEmpty() ? null : media.GetValueOrDefault(app.IconId)?.Thumbnails.GetValueOrDefault("thumb")
|
||||
Image = app.IconId.IsNullOrEmpty() ? null : media.GetValueOrDefault(app.IconId)?.ImageMeta?.Thumbnails.GetValueOrDefault("thumb")
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public class Media_ByParent : ZeroMultiMapIndex<MediaListItem>
|
||||
CreatedDate = item.CreatedDate,
|
||||
IsFolder = false,
|
||||
Name = item.Name,
|
||||
Image = item.Thumbnails["thumb"],
|
||||
Image = item.ImageMeta != null ? item.ImageMeta.Thumbnails["thumb"] : null,
|
||||
Children = 0,
|
||||
Size = item.Size,
|
||||
IsShared = item.Blueprint != null,
|
||||
|
||||
@@ -27,7 +27,7 @@ public class MediaManagement : IMediaManagement
|
||||
|
||||
if (!thumbnailKey.IsNullOrWhiteSpace())
|
||||
{
|
||||
path = file?.Thumbnails?.GetValueOrDefault(thumbnailKey);
|
||||
path = file?.ImageMeta?.Thumbnails?.GetValueOrDefault(thumbnailKey);
|
||||
}
|
||||
|
||||
if (path.IsNullOrEmpty())
|
||||
@@ -46,7 +46,7 @@ public class MediaManagement : IMediaManagement
|
||||
|
||||
if (!thumbnailKey.IsNullOrWhiteSpace())
|
||||
{
|
||||
path = file?.Thumbnails?.GetValueOrDefault(thumbnailKey);
|
||||
path = file?.ImageMeta?.Thumbnails?.GetValueOrDefault(thumbnailKey);
|
||||
}
|
||||
|
||||
if (path.IsNullOrEmpty())
|
||||
|
||||
@@ -26,11 +26,6 @@ public class Media : ZeroEntity, ISupportsTrees
|
||||
/// </summary>
|
||||
public string ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Alternative text which is used when the image can't be loaded
|
||||
/// </summary>
|
||||
public string AlternativeText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional caption text
|
||||
/// </summary>
|
||||
@@ -41,12 +36,6 @@ public class Media : ZeroEntity, ISupportsTrees
|
||||
/// </summary>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Define custom thumbnails which are generated on upload
|
||||
/// (see IZeroOptions.For<MediaOptions>().Thumbnails)
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Thumbnails { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Filesize in bytes
|
||||
/// </summary>
|
||||
@@ -56,9 +45,4 @@ public class Media : ZeroEntity, ISupportsTrees
|
||||
/// Meta data for images
|
||||
/// </summary>
|
||||
public MediaImageMetadata ImageMeta { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional focal point for an image
|
||||
/// </summary>
|
||||
public MediaFocalPoint FocalPoint { get; set; }
|
||||
}
|
||||
@@ -5,6 +5,22 @@
|
||||
/// </summary>
|
||||
public class MediaImageMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Alternative text which is used when the image can't be loaded
|
||||
/// </summary>
|
||||
public string AlternativeText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Define custom thumbnails which are generated on upload
|
||||
/// (see IZeroOptions.For<MediaOptions>().Thumbnails)
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Thumbnails { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Optional focal point for an image
|
||||
/// </summary>
|
||||
public MediaFocalPoint FocalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Width in pixels
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user