file upload works
This commit is contained in:
@@ -41,13 +41,6 @@ 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")]
|
||||
@@ -223,10 +216,18 @@ public class MediaController : ZeroApiTreeEntityStoreController<zero.Media.Media
|
||||
|
||||
[HttpPost("")]
|
||||
[ZeroAuthorize(MediaPermissions.Create)]
|
||||
public virtual Task<ActionResult<Result>> Create(zero.Media.Media saveModel)
|
||||
public virtual async Task<ActionResult<Result>> Create([FromForm] IFormFile file, [FromForm] string folderId = null)
|
||||
{
|
||||
saveModel.IsFolder = false;
|
||||
return CreateModel(saveModel);
|
||||
Result<zero.Media.Media> result = await Media.UploadFile(file, folderId);
|
||||
|
||||
bool minimalResponse = Hints.ResponsePreference == ApiResponsePreference.Minimal;
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return Created(GetAction(result.Model), minimalResponse ? null : result.Model);
|
||||
}
|
||||
|
||||
return result.WithoutModel();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -127,6 +127,11 @@ public class ApiResponseFilterAttribute : ResultFilterAttribute
|
||||
|
||||
ApiResponseMetadata GetMetadata(ResultExecutingContext context)
|
||||
{
|
||||
if (!context.HttpContext.Items.ContainsKey("zero.action.started"))
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
DateTimeOffset started = (DateTimeOffset)context.HttpContext.Items["zero.action.started"];
|
||||
TimeSpan duration = DateTimeOffset.Now - started;
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import uiTableFilter from './ui-table-filter.vue';
|
||||
import uiTrinity from './ui-trinity.vue';
|
||||
import uiPick from './ui-pick.vue';
|
||||
import uiDatagrid from './ui-datagrid.vue';
|
||||
import uiProgress from './ui-progress.vue';
|
||||
|
||||
export {
|
||||
uiIcon,
|
||||
@@ -53,5 +54,6 @@ export {
|
||||
uiTableFilter,
|
||||
uiTrinity,
|
||||
uiPick,
|
||||
uiDatagrid
|
||||
uiDatagrid,
|
||||
uiProgress
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="ui-progress" :class="{'is-animated': animated }">
|
||||
<span class="-value" :style="{ transform: 'translateX(' + ((100 - value) * -1) + '%)' }"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uiProgress',
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
progress()
|
||||
{
|
||||
return this.value > 100 ? 100 : (this.value < 0 ? 0 : this.value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.ui-progress
|
||||
{
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-bg-shade-3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-progress .-value
|
||||
{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform: translateX(-100%);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--color-accent);
|
||||
transition: transform 0.3s linear;
|
||||
}
|
||||
|
||||
.ui-progress.is-animated .-value:before
|
||||
{
|
||||
--color-progress-overlay: #00000022;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
right: -20px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-image: linear-gradient(-60deg, transparent 25%, var(--color-progress-overlay) 25%, var(--color-progress-overlay) 50%, transparent 50%, transparent 75%, var(--color-progress-overlay) 75%, var(--color-progress-overlay));
|
||||
background-size: 20px 30px;
|
||||
animation: uiProgressAnimation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes uiProgressAnimation
|
||||
{
|
||||
from
|
||||
{
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
to
|
||||
{
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@ const files = {
|
||||
|
||||
getHierarchy: (id: string, config?: ApiRequestConfig) => get(`media/${id}/hierarchy`, { ...config }),
|
||||
|
||||
create: (model: any, config?: ApiRequestConfig) => post('media', model, config),
|
||||
//create: (model: any, config?: ApiRequestConfig) => post('media', model, config),
|
||||
|
||||
update: (model: any, config?: ApiRequestConfig) => put('media/' + model.id, model, config),
|
||||
|
||||
@@ -21,13 +21,17 @@ const files = {
|
||||
|
||||
//uploadtest: (model: any, config?: ApiRequestConfig) => post('media/uploadtest', model, config),
|
||||
|
||||
uploadtest: async (file, onProgress) =>
|
||||
upload: async (file: File, folderId?: string, onProgress?: any) =>
|
||||
{
|
||||
var data = new FormData();
|
||||
data.append('file', file);
|
||||
//data.append('folderId', folderId);
|
||||
|
||||
return await post('media/uploadtest', data, {
|
||||
if (folderId)
|
||||
{
|
||||
data.append('folderId', folderId);
|
||||
}
|
||||
|
||||
return await post('media', data, {
|
||||
onUploadProgress: (progressEvent) =>
|
||||
{
|
||||
if (typeof onProgress === 'function')
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div class="ui-media-upload-item" v-if="loaded">
|
||||
<img class="-preview" v-if="preview" :src="preview" />
|
||||
<span class="-preview" v-if="!preview"><ui-icon symbol="fth-file" :size="18" /><span class="-ext">{{extension}}</span></span>
|
||||
<p class="ui-media-upload-item-text">
|
||||
<span class="-text">{{file.name}}</span>
|
||||
<span class="-minor">
|
||||
<span v-filesize="file.size"></span>
|
||||
<!--<span v-if="item.progress < 101 && !item.error && !item.success" class="ui-media-upload-item-progress">
|
||||
<span class="-inner" :style="{ width: item.progress + '%' }"></span>
|
||||
</span>-->
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import api from '../api';
|
||||
import { generateId } from '../../../utils/numbers';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
props: {
|
||||
file: File
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
preview: null,
|
||||
extension: null,
|
||||
loaded: false,
|
||||
}),
|
||||
|
||||
|
||||
watch: {
|
||||
file: {
|
||||
deep: true,
|
||||
handler()
|
||||
{
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
mounted()
|
||||
{
|
||||
this.render();
|
||||
},
|
||||
|
||||
|
||||
beforeUnmount()
|
||||
{
|
||||
if (this.preview)
|
||||
{
|
||||
URL.revokeObjectURL(this.preview);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
|
||||
render()
|
||||
{
|
||||
this.loaded = false;
|
||||
|
||||
if (!this.file)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let name = this.file.name;
|
||||
let dotIndex = name.lastIndexOf('.');
|
||||
|
||||
if (dotIndex > -1 && dotIndex + 6 > name.length)
|
||||
{
|
||||
this.extension = name.substring(dotIndex + 1, name.length);
|
||||
}
|
||||
|
||||
if (this.file.type.indexOf('image/') === 0)
|
||||
{
|
||||
this.preview = URL.createObjectURL(this.file);
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//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];
|
||||
// let preview = null;
|
||||
|
||||
// if (file.type.indexOf('image/') === 0)
|
||||
// {
|
||||
// let reader = new FileReader();
|
||||
// reader.onload = e => preview = e.target.result;
|
||||
// reader.readAsDataURL(file);
|
||||
// }
|
||||
|
||||
// this.items.push({
|
||||
// id: 'upload:' + generateId(),
|
||||
// name: file.name,
|
||||
// size: file.size,
|
||||
// mimeType: file.type,
|
||||
// preview: preview,
|
||||
// source: null,
|
||||
// progress: 0,
|
||||
// file: file,
|
||||
// isImage: false,
|
||||
// success: false,
|
||||
// error: null
|
||||
// });
|
||||
//}
|
||||
|
||||
//console.info(this.items);
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.ui-media-upload-item
|
||||
{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 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: 50px;
|
||||
width: 50px;
|
||||
object-fit: cover;
|
||||
background: var(--color-bg-shade-2);
|
||||
border-radius: var(--radius);
|
||||
position: relative;
|
||||
text-align: center;
|
||||
font-size: 22px;
|
||||
}
|
||||
/*&.is-upload .-preview
|
||||
{
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-text);
|
||||
}*/
|
||||
|
||||
|
||||
.-ext
|
||||
{
|
||||
font-size: 8px;
|
||||
color: var(--color-text-dim);
|
||||
font-style: normal;
|
||||
margin-top: 4px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: -4px;
|
||||
}
|
||||
|
||||
.-text
|
||||
{
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.-minor
|
||||
{
|
||||
display: block;
|
||||
color: var(--color-text-dim);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.ui-media-upload-item-text
|
||||
{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -1,27 +1,20 @@
|
||||
<template>
|
||||
<div class="ui-media-upload">
|
||||
<h2 class="ui-headline" v-localize="'Upload status'"></h2>
|
||||
<h2 class="ui-headline" v-localize="'@media.upload.headline'"></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 class="ui-media-upload-progress">
|
||||
<ui-progress :value="progress" :animated="!finished" />
|
||||
<p class="ui-media-upload-progress-text" v-if="!finished">Uploading {{completed + 1}} of {{fileCount}} ...</p>
|
||||
<p class="ui-media-upload-progress-text" v-else>Completed</p>
|
||||
<!--<upload-status-item :file="model[4]" />-->
|
||||
</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 class="ui-media-upload-items">
|
||||
<upload-status-item v-for="file in model.files" :file="file" />
|
||||
</div>-->
|
||||
|
||||
<div class="app-confirm-buttons" v-if="finished && hasErrors">
|
||||
<ui-button type="light" label="@ui.close" :disabled="loading" @click="config.confirm(null)"></ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -30,84 +23,74 @@
|
||||
<script lang="ts">
|
||||
import api from '../api';
|
||||
import { generateId } from '../../../utils/numbers';
|
||||
import UploadStatusItem from './upload-status-item.vue';
|
||||
|
||||
export default {
|
||||
|
||||
props: {
|
||||
model: FileList,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
files: [],
|
||||
folderId: null
|
||||
})
|
||||
},
|
||||
config: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
progress: 0,
|
||||
completed: 0,
|
||||
loading: false,
|
||||
items: []
|
||||
items: [],
|
||||
hasErrors: false
|
||||
}),
|
||||
|
||||
|
||||
mounted()
|
||||
components: { UploadStatusItem },
|
||||
|
||||
|
||||
computed: {
|
||||
fileCount()
|
||||
{
|
||||
return this.model.files.length;
|
||||
},
|
||||
finished()
|
||||
{
|
||||
return this.completed >= this.fileCount;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
async 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
|
||||
});
|
||||
}
|
||||
await this.upload();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
upload()
|
||||
async upload()
|
||||
{
|
||||
for (var i = 0; i < this.items.length; i++)
|
||||
const files = [...this.model.files];
|
||||
const combinedSize = files.reduce((prev, current, idx) => prev + current.size, 0);
|
||||
|
||||
for (const file of files)
|
||||
{
|
||||
let item = this.items[i];
|
||||
const factor = file.size / combinedSize;
|
||||
const progressBefore = this.progress;
|
||||
|
||||
api.uploadtest(item.file, progress =>
|
||||
await api.upload(file, this.model.folderId, 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
|
||||
//}
|
||||
this.progress = progressBefore + progress * factor;
|
||||
});
|
||||
|
||||
this.completed += 1;
|
||||
}
|
||||
},
|
||||
|
||||
onSubmit()
|
||||
{
|
||||
|
||||
if (!this.hasErrors)
|
||||
{
|
||||
this.config.confirm(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,74 +119,12 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ui-media-upload-item
|
||||
.ui-media-upload-progress-text
|
||||
{
|
||||
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);
|
||||
}
|
||||
margin-top: var(--padding-xs);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--color-text-dim);
|
||||
}
|
||||
|
||||
.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,7 +1,10 @@
|
||||
<template>
|
||||
<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" />
|
||||
<form enctype="multipart/form-data" class="media-overview-drop-outer" dropzone=""
|
||||
@dragenter.prevent="onDragEnter" @dragleave.prevent="onDragLeave" @dragover.prevent="" @drop.prevent.stop="onDrop($event.dataTransfer.files)">
|
||||
<div class="media-overview-drop" :class="{ 'is-dragging': dragging }">
|
||||
<ui-icon symbol="fth-upload" :size="26" :stroke-width="2" />
|
||||
<input type="file" multiple class="-input" @change="onDrop($event.target.files)" />
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
@@ -12,7 +15,10 @@
|
||||
name: 'mediaOverviewDrop',
|
||||
|
||||
props: {
|
||||
|
||||
folderId: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
@@ -22,15 +28,24 @@
|
||||
|
||||
methods: {
|
||||
|
||||
async onDrop(ev)
|
||||
async onDrop(files)
|
||||
{
|
||||
console.info(ev.dataTransfer);
|
||||
this.dragging = false;
|
||||
|
||||
const result = await overlays.open({
|
||||
component: () => import('../../overlays/upload-status.vue'),
|
||||
model: ev.dataTransfer.files
|
||||
width: 420,
|
||||
softdismiss: false,
|
||||
model: {
|
||||
files,
|
||||
folderId: this.folderId
|
||||
}
|
||||
});
|
||||
|
||||
if (result.eventType === 'confirm')
|
||||
{
|
||||
this.$emit('completed', files);
|
||||
}
|
||||
},
|
||||
|
||||
onDragEnter(ev)
|
||||
@@ -60,11 +75,30 @@
|
||||
box-shadow: var(--shadow-short);
|
||||
border: 1px dashed var(--color-line-dashed-onbg);
|
||||
color: var(--color-text);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-dragging
|
||||
{
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.-input
|
||||
{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 130px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
input[type=file],
|
||||
input[type=file]::-webkit-file-upload-button
|
||||
{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-datagrid-outer.is-selecting .media-overview-drop
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<ui-datagrid ref="grid" v-model="gridConfig" @select="onSelected" @count="count = $event">
|
||||
|
||||
<template v-slot:before>
|
||||
<media-drop />
|
||||
<media-drop :folder-id="parentId" @completed="refresh" />
|
||||
</template>
|
||||
|
||||
<template v-if="selected.length < 1" v-slot:actions="props">
|
||||
@@ -125,6 +125,12 @@
|
||||
},
|
||||
|
||||
|
||||
async refresh()
|
||||
{
|
||||
await this.$refs.grid.update();
|
||||
},
|
||||
|
||||
|
||||
onSelected(selection)
|
||||
{
|
||||
this.selected = selection;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="app-overlay-outer" :display="instance.display" v-for="(instance, index) in instances" :key="instance.id"
|
||||
:style="{ transform: instance.display !== 'editor' ? null : 'translateX(' + (editorLength - index - 1) * -120 + 'px)' }"
|
||||
:class="instance.class || ''">
|
||||
<div class="app-overlay-bg" @click="remove(instance)"></div>
|
||||
<div class="app-overlay-bg" @click="tryRemove(instance)"></div>
|
||||
<div open class="app-overlay" :data-alias="instance.alias" :style="{ width: instance.width ? (instance.width + 'px') : null }" :class="'theme-' + instance.theme" :display="instance.display">
|
||||
<component :is="instance.component" :model.sync="instance.model" :config="instance" v-bind="instance" title=""></component>
|
||||
</div>
|
||||
@@ -57,6 +57,14 @@
|
||||
this.instances.push(instance);
|
||||
},
|
||||
|
||||
tryRemove(instance)
|
||||
{
|
||||
if (instance.softdismiss)
|
||||
{
|
||||
this.remove(instance);
|
||||
}
|
||||
},
|
||||
|
||||
remove(instance)
|
||||
{
|
||||
emitter.emit(event_finalizeOverlay, { eventType: 'close', instance, force: true });
|
||||
@@ -121,7 +129,6 @@
|
||||
border: none !important;
|
||||
box-shadow: var(--shadow-overlay-dialog);
|
||||
padding: var(--padding);
|
||||
padding-top: 40px;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
-webkit-backface-visibility: hidden;
|
||||
@@ -141,7 +148,7 @@
|
||||
|
||||
.app-overlay .ui-headline
|
||||
{
|
||||
margin-bottom: var(--padding-m);
|
||||
margin-bottom: var(--padding);
|
||||
}
|
||||
|
||||
.app-overlay[display="dialog"] .ui-form-loading
|
||||
|
||||
@@ -681,7 +681,12 @@
|
||||
}
|
||||
},
|
||||
"child_count_1": "{count} item",
|
||||
"child_count_x": "{count} items"
|
||||
"child_count_x": "{count} items",
|
||||
"upload": {
|
||||
"headline": "Upload",
|
||||
"multipleHeadline": "Uploading {count} items...",
|
||||
"singleHeadline": "Uploading..."
|
||||
}
|
||||
},
|
||||
|
||||
"mailTemplate": {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using SixLabors.ImageSharp;
|
||||
using Shorthand.ImageSharp.WebP;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.Formats.Jpeg;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using System.IO;
|
||||
|
||||
namespace zero.Media;
|
||||
@@ -64,50 +68,38 @@ public class MediaCreator : IMediaCreator
|
||||
using Image<Rgba32> image = await Image.LoadAsync<Rgba32>(fileInfo.AbsolutePath);
|
||||
model.ImageMeta = GetImageMetadata(image);
|
||||
|
||||
// TODO save thumbnails
|
||||
string extension = Path.GetExtension(model.Path);
|
||||
|
||||
foreach ((string key, ResizeOptions opts) in Options.Thumbnails)
|
||||
{
|
||||
Image<Rgba32> imageFrame = image.Frames.Count > 1 ? image.Frames.CloneFrame(0) : image.Clone();
|
||||
imageFrame.Mutate(x => x.Resize(opts));
|
||||
|
||||
using MemoryStream stream = new();
|
||||
//image.Save(stream, new WebPEncoder()
|
||||
//{
|
||||
// Quality = 70
|
||||
//});
|
||||
await imageFrame.SaveAsync(stream, new JpegEncoder()
|
||||
{
|
||||
Quality = 80
|
||||
}, cancellationToken);
|
||||
|
||||
stream.Position = 0;
|
||||
|
||||
string thumbFilename = normalizedFilename.TrimEnd(extension) + "." + Safenames.File(key) + ".jpg";
|
||||
string path = directory + '/' + thumbFilename;
|
||||
|
||||
await FileSystem.CreateFile(path, stream, cancellationToken: cancellationToken);
|
||||
|
||||
model.ImageMeta.Thumbnails[key] = path;
|
||||
}
|
||||
}
|
||||
|
||||
return Result<Media>.Success(model);
|
||||
}
|
||||
|
||||
|
||||
//public virtual async Task<string> CreateImageThumbnails(Image<Rgba32> image, CancellationToken cancellationToken = default)
|
||||
//{
|
||||
// foreach ((string key, ResizeOptions opts) in Options.Thumbnails)
|
||||
// {
|
||||
// Image<Rgba32> imageFrame = image.Frames.Count > 1 ? image.Frames.CloneFrame(0) : image.Clone();
|
||||
// imageFrame.Mutate(x => x.Resize(opts));
|
||||
// }
|
||||
// string source = SaveThumbnail(media, imageFrame, PREVIEW_EXTENSION, new ResizeOptions()
|
||||
// {
|
||||
// Size = new Size(210, 210),
|
||||
// Mode = ResizeMode.Min
|
||||
// });
|
||||
|
||||
// //media.ThumbnailSource = SaveThumbnail(media, imageFrame, THUMB_EXTENSION, new ResizeOptions()
|
||||
// //{
|
||||
// // Size = new Size(100, 100),
|
||||
// // Mode = ResizeMode.Max
|
||||
// //});
|
||||
//}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Saves a thumbnail of an image
|
||||
/// </summary>
|
||||
//public virtual string SaveThumbnail(Media media, Image<Rgba32> image, string extensionPrefix, ResizeOptions resizeOptions)
|
||||
//{
|
||||
// string extension = Path.GetExtension(media.Path);
|
||||
|
||||
// image.Mutate(x => x.Resize(resizeOptions));
|
||||
|
||||
// string thumbFileName = media.Name.TrimEnd(extension) + extensionPrefix + extension;
|
||||
// image.Save()
|
||||
// image.Save(Path.Combine(Paths.Media, media.FileId, thumbFileName));
|
||||
// return Path.Combine(PATH_PREFIX, media.FileId, thumbFileName).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR);
|
||||
//}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected virtual MediaImageMetadata GetImageMetadata(Image<Rgba32> image)
|
||||
{
|
||||
|
||||
@@ -85,7 +85,14 @@ public class MediaManagement : IMediaManagement
|
||||
/// <inheritdoc />
|
||||
public virtual async Task<Result<Media>> UploadFile(Stream fileStream, string filename, string folderId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Creator.UploadFile(fileStream, filename, folderId, cancellationToken);
|
||||
Result<Media> result = await Creator.UploadFile(fileStream, filename, folderId, cancellationToken);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await Store.Create(result.Model);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="RavenDB.Client" Version="5.2.4" />
|
||||
<PackageReference Include="Shorthand.ImageSharp.WebP" Version="2.2.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user