Browse Source

Assets improved

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
e7cc1a84c1
  1. 1
      .gitignore
  2. 2
      src/Squidex.Core/Squidex.Core.csproj
  3. 2
      src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs
  4. 54
      src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs
  5. 2
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  6. 7
      src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs
  7. BIN
      src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0
  8. BIN
      src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0
  9. BIN
      src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0
  10. 4
      src/Squidex/Config/Swagger/XmlTagProcessor.cs
  11. 107
      src/Squidex/Controllers/Api/Assets/AssetContentController.cs
  12. 1
      src/Squidex/Controllers/Api/Assets/AssetController.cs
  13. 6
      src/Squidex/Squidex.csproj
  14. 15
      src/Squidex/app/features/assets/pages/asset.component.html
  15. 24
      src/Squidex/app/features/assets/pages/asset.component.scss
  16. 22
      src/Squidex/app/features/assets/pages/asset.component.ts
  17. 39
      src/Squidex/app/features/assets/pages/assets-page.component.html
  18. 24
      src/Squidex/app/features/assets/pages/assets-page.component.scss
  19. 12
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  20. 13
      src/Squidex/app/framework/angular/file-drop.directive.ts

1
.gitignore

@ -20,3 +20,4 @@ node_modules/
**/wwwroot/scripts/**/*.*
/src/Squidex/appsettings.Development.json
/src/Squidex/Assets

2
src/Squidex.Core/Squidex.Core.csproj

@ -14,7 +14,7 @@
<PackageReference Include="protobuf-net" Version="2.1.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="NodaTime" Version="2.0.0" />
<PackageReference Include="NJsonSchema" Version="8.27.6302.16041" />
<PackageReference Include="NJsonSchema" Version="8.30.6304.31883" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.6' ">
<PackageReference Include="Microsoft.OData.Core" Version="6.15.0" />

2
src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs

@ -15,6 +15,6 @@ namespace Squidex.Infrastructure.Assets
{
Task<ImageInfo> GetImageInfoAsync(Stream input);
Task<Stream> GetThumbnailOrNullAsync(Stream input, int dimension);
Task<Stream> CreateThumbnailAsync(Stream input, int? width, int? height, string mode);
}
}

54
src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
using ImageSharp;
@ -22,24 +23,46 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
Configuration.Default.AddImageFormat(new PngFormat());
}
public Task<Stream> GetThumbnailOrNullAsync(Stream input, int dimension)
public Task<Stream> CreateThumbnailAsync(Stream input, int? width, int? height, string mode)
{
return Task.Run(() =>
{
if (width == null && height == null)
{
return input;
}
if (!Enum.TryParse<ResizeMode>(mode, true, out var resizeMode))
{
resizeMode = ResizeMode.Max;
}
var w = width ?? int.MaxValue;
var h = height ?? int.MaxValue;
var result = new MemoryStream();
var options =
new ResizeOptions
using (var sourceImage = Image.Load(input))
{
if (w >= sourceImage.Width && h >= sourceImage.Height && resizeMode == ResizeMode.Crop)
{
Size = new Size(dimension, dimension),
Mode = ResizeMode.Max
};
resizeMode = ResizeMode.BoxPad;
}
var image = new Image(input).Resize(options);
var options =
new ResizeOptions
{
Size = new Size(w, h),
Mode = resizeMode
};
sourceImage.MetaData.Quality = 0;
sourceImage.Resize(options).Save(result);
}
image.Save(result);
result.Position = 0;
return (Stream)result;
return result;
});
}
@ -47,23 +70,22 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
{
return Task.Run(() =>
{
ImageInfo imageInfo = null;
try
{
var image = new Image(input);
var image = Image.Load(input);
if (image.Width > 0 && image.Height > 0)
{
return new ImageInfo(image.Width, image.Height);
}
else
{
return null;
imageInfo = new ImageInfo(image.Width, image.Height);
}
}
catch
{
return null;
imageInfo = null;
}
return imageInfo;
});
}
}

2
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -8,7 +8,7 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ImageSharp" Version="1.0.0-alpha4-00031" />
<PackageReference Include="ImageSharp" Version="1.0.0-alpha5-00054" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />

7
src/Squidex.Read.MongoDb/Assets/MongoAssetRepository.cs

@ -26,12 +26,17 @@ namespace Squidex.Read.MongoDb.Assets
{
}
protected override string CollectionName()
{
return "Projections_Assets";
}
public async Task<IReadOnlyList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, string query = null, int take = 10, int skip = 0)
{
var filter = CreateFilter(appId, mimeTypes, query);
var assets =
await Collection.Find(filter).Skip(skip).Limit(take).ToListAsync();
await Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.LastModified).ToListAsync();
return assets.OfType<IAssetEntity>().ToList();
}

BIN
src/Squidex/Assets/776fe199-0beb-4d64-b10c-01bdda742123_0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

BIN
src/Squidex/Assets/84ede32b-edd6-4859-8ca2-4de831a5d305_0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

BIN
src/Squidex/Assets/c1a2f5ee-df8e-4930-932d-031bcbdf959d_0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

4
src/Squidex/Config/Swagger/XmlTagProcessor.cs

@ -20,7 +20,7 @@ namespace Squidex.Config.Swagger
{
public sealed class XmlTagProcessor : IOperationProcessor, IDocumentProcessor
{
public void Process(DocumentProcessorContext context)
public Task ProcessAsync(DocumentProcessorContext context)
{
foreach (var controllerType in context.ControllerTypes)
{
@ -46,6 +46,8 @@ namespace Squidex.Config.Swagger
}
}
}
return TaskHelper.Done;
}
public Task<bool> ProcessAsync(OperationProcessorContext context)

107
src/Squidex/Controllers/Api/Assets/AssetContentController.cs

@ -0,0 +1,107 @@
// ==========================================================================
// AssetContentController.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Pipeline;
using Squidex.Read.Assets.Repositories;
#pragma warning disable 1573
namespace Squidex.Controllers.Api.Assets
{
/// <summary>
/// Uploads and retrieves assets.
/// </summary>
[ApiExceptionFilter]
[ServiceFilter(typeof(AppFilterAttribute))]
[SwaggerTag("Assets")]
public class AssetContentController : ControllerBase
{
private readonly IAssetStore assetStorage;
private readonly IAssetRepository assetRepository;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
public AssetContentController(
ICommandBus commandBus,
IAssetStore assetStorage,
IAssetRepository assetRepository,
IAssetThumbnailGenerator assetThumbnailGenerator)
: base(commandBus)
{
this.assetStorage = assetStorage;
this.assetRepository = assetRepository;
this.assetThumbnailGenerator = assetThumbnailGenerator;
}
/// <summary>
/// Gets the content of the asset.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="mode">The resize mode.</param>
/// <param name="width">The target width of the image.</param>
/// <param name="height">The target width of the image.</param>
/// <returns>
/// 200 => Asset content returned.
/// 404 => App or Asset not found.
/// </returns>
[HttpGet]
[Route("assets/{id}/")]
public async Task<IActionResult> GetAssetContent(string app, Guid id, [FromQuery] int? width = null, [FromQuery] int? height = null, [FromQuery] string mode = null)
{
var asset = await assetRepository.FindAssetAsync(id);
if (asset == null)
{
return NotFound();
}
Stream content;
if (asset.IsImage && (width.HasValue || height.HasValue))
{
var name = $"{asset.Id}_{asset.Version}_{width}_{height}_{mode}";
content = await assetStorage.GetAssetAsync(name);
if (content == null)
{
var fullSizeContent = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}");
if (fullSizeContent == null)
{
return NotFound();
}
content = await assetThumbnailGenerator.CreateThumbnailAsync(fullSizeContent, width, height, mode);
await assetStorage.UploadAssetAsync(name, content);
content.Position = 0;
}
}
else
{
content = await assetStorage.GetAssetAsync($"{asset.Id}_{asset.Version}");
}
if (content == null)
{
return NotFound();
}
return new FileStreamResult(content, asset.MimeType);
}
}
}

1
src/Squidex/Controllers/Api/Assets/AssetController.cs

@ -105,7 +105,6 @@ namespace Squidex.Controllers.Api.Assets
[HttpPost]
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 409)]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PostAsset(string app, List<IFormFile> files)
{

6
src/Squidex/Squidex.csproj

@ -33,7 +33,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.4.0" />
<PackageReference Include="Autofac" Version="4.5.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.0.0" />
<PackageReference Include="IdentityServer4" Version="1.4.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.1.0" />
@ -57,9 +57,9 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.1" />
<PackageReference Include="MongoDB.Driver" Version="2.4.3" />
<PackageReference Include="NJsonSchema" Version="8.27.6302.16041" />
<PackageReference Include="NJsonSchema" Version="8.30.6304.31883" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="9.12.0" />
<PackageReference Include="NSwag.AspNetCore" Version="10.0.0" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="ReportGenerator" Version="2.5.6" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" />

15
src/Squidex/app/features/assets/pages/asset.component.html

@ -1,7 +1,12 @@
<div class="card">
<div class="card-block">
</div>
<div class="card-footer">
{{fileInfo}}
<div class="asset">
<div class="card">
<div class="card-block">
<div *ngIf="asset && asset.isImage">
<img [attr.src]="previewUrl | async">
</div>
</div>
<div class="card-footer">
{{fileInfo}}
</div>
</div>
</div>

24
src/Squidex/app/features/assets/pages/asset.component.scss

@ -1,11 +1,25 @@
@import '_vars';
@import '_mixins';
$card-size: 16rem;
$card-size: 240px;
:host {
padding-bottom: 1rem;
}
.card {
width: $card-size;
height: $card-size;
margin-right: 1rem;
margin-bottom: 1rem;
& {
height: $card-size;
}
&-block {
padding: .8rem .8rem 0;
}
&-footer {
border: 0;
font-size: .8rem;
font-weight: normal;
padding: .8rem;
}
}

22
src/Squidex/app/features/assets/pages/asset.component.ts

@ -5,9 +5,11 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, NgZone, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import {
ApiUrlConfig,
AppComponentBase,
AppsStoreService,
AssetDto,
@ -28,6 +30,10 @@ export class AssetComponent extends AppComponentBase implements OnInit {
@Input()
public asset: AssetDto;
public get previewUrl(): Observable<string> {
return this.appName().map(app => this.apiUrl.buildUrl(`api/assets/${this.asset.id}/?width=230&height=140&mode=Crop`));
}
public get fileInfo(): string {
let result = '';
@ -43,7 +49,9 @@ export class AssetComponent extends AppComponentBase implements OnInit {
}
constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService,
private readonly assetsService: AssetsService
private readonly assetsService: AssetsService,
private readonly apiUrl: ApiUrlConfig,
private readonly zone: NgZone
) {
super(notifications, users, apps);
}
@ -53,11 +61,13 @@ export class AssetComponent extends AppComponentBase implements OnInit {
if (initFile) {
this.appName()
.switchMap(app => this.assetsService.uploadFile(app, initFile))
.switchMap(app => this.assetsService.uploadFile(app, initFile)).delay(2000)
.subscribe(result => {
if (result instanceof AssetDto) {
this.asset = result;
}
this.zone.run(() => {
if (result instanceof AssetDto) {
this.asset = result;
}
});
}, error => {
this.notifyError(error);
});

39
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -1,8 +1,14 @@
<sqx-title message="{app} | Assets" parameter1="app" value1="{{appName() | async}}"></sqx-title>
<sqx-panel panelWidth="80rem">
<sqx-panel panelWidth="71.5rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" [formControl]="assetsFilter" placeholder="Search for assets" />
</form>
</div>
<h3 class="panel-title">Assets</h3>
</div>
@ -13,25 +19,38 @@
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div class="file-drop" (sqxFileDrop)="onDrop($event)">
<div class="file-drop" (sqxFileDrop)="addFiles($event)">
<h3 class="file-drop-header">Drop files here to upload</h3>
<div class="file-drop-or">or</div>
<div class="file-drop-button">
<button class="btn btn-success">Select Files</button>
<span class="btn btn-success" (click)="fileInput.click()">
<span>Select File</span>
<input class="btn-input" type="file" (change)="addFiles($event.target.files)" #fileInput />
</span>
</div>
<div class="file-drop-info">Drop file on existing item to replace the asset with a newer version.</div>
</div>
<div>
<span *ngFor="let file of newFiles">
<sqx-asset [initFile]="file"></sqx-asset>
</span>
<span *ngFor="let asset of assetsItems">
<sqx-asset [asset]="asset"></sqx-asset>
</span>
<div class="row">
<sqx-asset class="col-3" *ngFor="let file of newFiles" [initFile]="file"></sqx-asset>
<sqx-asset class="col-3" *ngFor="let asset of assetsItems" [asset]="asset"></sqx-asset>
</div>
<div class="clearfix" *ngIf="assetsPager.numberOfItems > 0">
<div class="float-right pagination">
<span class="pagination-text">{{assetsPager.itemFirst}}-{{assetsPager.itemLast}} of {{assetsPager.numberOfItems}}</span>
<button class="btn btn-simple pagination-button" [disabled]="!assetsPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i>
</button>
<button class="btn btn-simple pagination-button" [disabled]="!assetsPager.canGoNext" (click)="goNext()">
<i class="icon-angle-right"></i>
</button>
</div>
</div>
</div>
</div>

24
src/Squidex/app/features/assets/pages/assets-page.component.scss

@ -8,6 +8,7 @@
padding: 1rem;
text-align: center;
margin-bottom: 1rem;
margin-right: 0;
}
&-or {
@ -21,4 +22,27 @@
&-info {
color: $color-subtext;
}
}
.btn {
cursor: pointer;
}
.btn-input {
width: 0;
height: 0;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
.row {
margin-left: -8px;
margin-right: -8px;
}
.col-3 {
padding-left: 8px;
padding-right: 8px;
}

12
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -5,6 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
// tslint:disable:prefer-for-of
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
@ -32,7 +34,7 @@ export class AssetsPageComponent extends AppComponentBase implements OnInit {
public newFiles = ImmutableArray.empty<File>();
public assetsItems = ImmutableArray.empty<AssetDto>();
public assetsPager = new Pager(0);
public assetsPager = new Pager(0, 0, 12);
public assetsFilter = new FormControl();
public assertQuery = '';
@ -47,7 +49,7 @@ export class AssetsPageComponent extends AppComponentBase implements OnInit {
}
public search() {
this.assetsPager = new Pager(0);
this.assetsPager = new Pager(0, 0, 12);
this.assertQuery = this.assetsFilter.value;
this.load();
@ -76,9 +78,9 @@ export class AssetsPageComponent extends AppComponentBase implements OnInit {
this.load();
}
public onDrop(files: File[]) {
for (let file of files) {
this.newFiles = this.newFiles.pushFront(file);
public addFiles(files: FileList) {
for (let i = 0; i < files.length; i++) {
this.newFiles = this.newFiles.pushFront(files[i]);
}
}
}

13
src/Squidex/app/framework/angular/file-drop.directive.ts

@ -12,7 +12,7 @@ import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
})
export class FileDropDirective {
@Output('sqxFileDrop')
public drop = new EventEmitter<File[]>();
public drop = new EventEmitter<FileList>();
@HostListener('dragenter', ['$event'])
public onDragEnter(event: DragDropEvent) {
@ -26,16 +26,7 @@ export class FileDropDirective {
@HostListener('drop', ['$event'])
public onDrop(event: DragDropEvent) {
const files: File[] = [];
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < event.dataTransfer.files.length; i++) {
const file = event.dataTransfer.files[i];
files.push(file);
}
this.drop.emit(files);
this.drop.emit(event.dataTransfer.files);
this.stopEvent(event);
}

Loading…
Cancel
Save