398 lines
19 KiB
HTML
398 lines
19 KiB
HTML
<!--
|
|
|
|
Copyright © 2016-2024 The Thingsboard Authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
-->
|
|
<div class="tb-images tb-absolute-fill" [ngClass]="{'tb-dialog-mode': dialogMode, 'mat-padding': !dialogMode}">
|
|
<div fxFlex fxLayout="column" class="tb-images-content" [ngClass]="{'tb-outlined-border': pageMode}">
|
|
<mat-toolbar class="mat-mdc-table-toolbar"
|
|
fxLayout.lt-lg="column" fxLayoutAlign.lt-lg="start stretch"
|
|
[fxShow]="!textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())" [ngClass.lt-lg]="{'multi-row': !isSysAdmin}">
|
|
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
|
|
<div class="mat-toolbar-tools">
|
|
<span fxHide fxShow.gt-sm class="tb-images-title" translate>image.gallery</span>
|
|
<div class="tb-images-view-type-toolbar"
|
|
fxLayout="row"
|
|
fxLayoutAlign="start center">
|
|
<div class="tb-toolbar-button" [ngClass]="{'tb-selected' : mode === 'list'}">
|
|
<button mat-icon-button
|
|
(click)="setMode('list')"
|
|
matTooltip="{{'image.list-mode' | translate }}"
|
|
matTooltipPosition="below">
|
|
<mat-icon>view_list</mat-icon>
|
|
</button>
|
|
</div>
|
|
<div class="tb-toolbar-button" [ngClass]="{'tb-selected' : mode === 'grid'}">
|
|
<button mat-icon-button
|
|
(click)="setMode('grid');"
|
|
matTooltip="{{'image.grid-mode' | translate }}"
|
|
matTooltipPosition="below">
|
|
<tb-icon>mdi:view-grid</tb-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<mat-slide-toggle *ngIf="!isSysAdmin"
|
|
fxHide fxShow.gt-md
|
|
[ngModel]="includeSystemImages"
|
|
(ngModelChange)="includeSystemImagesChanged($event)">{{ 'image.include-system-images' | translate }}</mat-slide-toggle>
|
|
</div>
|
|
<section fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap.gt-md="16px">
|
|
<span fxFlex></span>
|
|
<section fxLayout="row" fxLayoutAlign="start center">
|
|
<button [disabled]="isLoading$ | async"
|
|
mat-icon-button
|
|
(click)="enterFilterMode()"
|
|
matTooltip="{{'action.search' | translate }}"
|
|
matTooltipPosition="above">
|
|
<mat-icon>search</mat-icon>
|
|
</button>
|
|
<button [disabled]="isLoading$ | async"
|
|
mat-icon-button
|
|
(click)="updateData()"
|
|
matTooltip="{{'action.refresh' | translate }}"
|
|
matTooltipPosition="above">
|
|
<mat-icon>refresh</mat-icon>
|
|
</button>
|
|
<button [disabled]="isLoading$ | async"
|
|
mat-icon-button
|
|
(click)="importImage()"
|
|
matTooltip="{{'image.import-image' | translate }}"
|
|
matTooltipPosition="above">
|
|
<tb-icon>mdi:file-import</tb-icon>
|
|
</button>
|
|
<button fxHide fxShow.lt-lg
|
|
[disabled]="isLoading$ | async"
|
|
mat-icon-button
|
|
color="primary"
|
|
(click)="uploadImage()"
|
|
matTooltip="{{'image.upload-image' | translate }}"
|
|
matTooltipPosition="above">
|
|
<mat-icon>add</mat-icon>
|
|
</button>
|
|
</section>
|
|
<button fxHide fxShow.gt-md
|
|
[disabled]="isLoading$ | async"
|
|
mat-flat-button
|
|
color="primary"
|
|
(click)="uploadImage()">
|
|
{{ 'image.upload-image' | translate }}
|
|
</button>
|
|
</section>
|
|
</div>
|
|
<mat-slide-toggle *ngIf="!isSysAdmin" fxHide fxShow.lt-lg
|
|
[ngModel]="includeSystemImages"
|
|
(ngModelChange)="includeSystemImagesChanged($event)">{{ 'image.include-system-images' | translate }}</mat-slide-toggle>
|
|
</mat-toolbar>
|
|
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode && (mode === 'grid' || dataSource?.selection.isEmpty())">
|
|
<div class="mat-toolbar-tools">
|
|
<button mat-icon-button
|
|
matTooltip="{{ 'image.search' | translate }}"
|
|
matTooltipPosition="above">
|
|
<mat-icon>search</mat-icon>
|
|
</button>
|
|
<mat-form-field fxFlex>
|
|
<mat-label> </mat-label>
|
|
<input #searchInput matInput
|
|
[formControl]="textSearch"
|
|
placeholder="{{ 'image.search' | translate }}"/>
|
|
</mat-form-field>
|
|
<button mat-icon-button (click)="exitFilterMode()"
|
|
matTooltip="{{ 'action.close' | translate }}"
|
|
matTooltipPosition="above">
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
</mat-toolbar>
|
|
<mat-toolbar class="mat-mdc-table-toolbar" color="primary" [fxShow]="mode === 'list' && !dataSource?.selection.isEmpty()">
|
|
<div class="mat-toolbar-tools">
|
|
<span>
|
|
{{ translate.get('image.selected-images', {count: dataSource?.selection.selected.length}) | async }}
|
|
</span>
|
|
<span fxFlex></span>
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ 'action.delete' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="deleteImages($event)">
|
|
<mat-icon>delete</mat-icon>
|
|
</button>
|
|
</div>
|
|
</mat-toolbar>
|
|
<div fxFlex *ngIf="mode === 'list'" fxLayout="column">
|
|
<div fxFlex class="table-container">
|
|
<table mat-table [dataSource]="dataSource" [trackBy]="trackByEntity"
|
|
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
|
|
<ng-container matColumnDef="select" sticky>
|
|
<mat-header-cell *matHeaderCellDef style="width: 30px;">
|
|
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
|
|
[checked]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async)"
|
|
[indeterminate]="dataSource.selection.hasValue() && (dataSource.isAllSelected() | async) === false">
|
|
</mat-checkbox>
|
|
</mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
<mat-checkbox (click)="$event.stopPropagation()"
|
|
[fxShow]="deleteEnabled(image)"
|
|
(change)="$event ? dataSource.selection.toggle(image) : null"
|
|
[checked]="dataSource.selection.isSelected(image)">
|
|
</mat-checkbox>
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="preview">
|
|
<mat-header-cell *matHeaderCellDef style="width: 50px; min-width: 50px;"></mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
<div class="tb-image-preview-cell">
|
|
<img class="tb-image-preview" [src]="image.link | image: {preview: true} | async" alt="{{ image.title }}">
|
|
</div>
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="title">
|
|
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 100%;"> {{ 'image.name' | translate }} </mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
{{ image.title }}
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="createdTime">
|
|
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 160px; min-width: 160px;"> {{ 'image.created-time' | translate }} </mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
{{ image.createdTime | date:'yyyy-MM-dd HH:mm:ss' }}
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="resolution">
|
|
<mat-header-cell *matHeaderCellDef style="width: 80px; min-width: 80px;"> {{ 'image.resolution' | translate }} </mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
{{ image.descriptor.width }}x{{ image.descriptor.height }}
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="size">
|
|
<mat-header-cell *matHeaderCellDef style="width: 80px; min-width: 80px;"> {{ 'image.size' | translate }} </mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
{{ image.descriptor.size | fileSize }}
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="system">
|
|
<mat-header-cell *matHeaderCellDef style="width: 60px; min-width: 60px;"> {{ 'image.system' | translate }} </mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
<mat-icon class="material-icons mat-icon">{{isSystem(image) ? 'check_box' : 'check_box_outline_blank'}}</mat-icon>
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="actions" stickyEnd>
|
|
<mat-header-cell *matHeaderCellDef [ngStyle.gt-md]="{ minWidth: '240px', maxWidth: '240px', width: '240px' }">
|
|
</mat-header-cell>
|
|
<mat-cell *matCellDef="let image" [ngStyle.gt-md]="{ minWidth: '240px', maxWidth: '240px', width: '240px' }">
|
|
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ 'image.download-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="downloadImage($event, image)">
|
|
<mat-icon>file_download</mat-icon>
|
|
</button>
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ 'image.export-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="exportImage($event, image)">
|
|
<tb-icon>mdi:file-export</tb-icon>
|
|
</button>
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ 'image.embed-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="embedImage($event, image)">
|
|
<mat-icon>code</mat-icon>
|
|
</button>
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ (readonly(image) ? 'image.image-details' : 'image.edit-image') | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="editImage($event, image)">
|
|
<mat-icon>edit</mat-icon>
|
|
</button>
|
|
<button mat-icon-button [disabled]="(isLoading$ | async) || !deleteEnabled(image)"
|
|
matTooltip="{{ 'image.delete-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="deleteImage($event, image)">
|
|
<mat-icon>delete</mat-icon>
|
|
</button>
|
|
</div>
|
|
<div fxHide fxShow.lt-lg>
|
|
<button mat-icon-button
|
|
(click)="$event.stopPropagation()"
|
|
[matMenuTriggerFor]="cellActionsMenu">
|
|
<mat-icon class="material-icons">more_vert</mat-icon>
|
|
</button>
|
|
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
|
|
<button mat-menu-item
|
|
[disabled]="isLoading$ | async"
|
|
(click)="downloadImage($event, image)">
|
|
<mat-icon>file_download</mat-icon>
|
|
<span translate>image.download-image</span>
|
|
</button>
|
|
<button mat-menu-item
|
|
[disabled]="isLoading$ | async"
|
|
(click)="exportImage($event, image)">
|
|
<tb-icon matMenuItemIcon>mdi:file-export</tb-icon>
|
|
<span translate>image.export-image</span>
|
|
</button>
|
|
<button mat-menu-item
|
|
[disabled]="isLoading$ | async"
|
|
(click)="embedImage($event, image)">
|
|
<mat-icon>code</mat-icon>
|
|
<span>{{ 'image.embed-image' | translate }}</span>
|
|
</button>
|
|
<button mat-menu-item
|
|
[disabled]="isLoading$ | async"
|
|
(click)="editImage($event, image)">
|
|
<mat-icon>edit</mat-icon>
|
|
<span>{{ (readonly(image) ? 'image.image-details' : 'image.edit-image') | translate }}</span>
|
|
</button>
|
|
<button mat-menu-item
|
|
[disabled]="isLoading$ | async"
|
|
[fxShow]="deleteEnabled(image)"
|
|
(click)="deleteImage($event, image)">
|
|
<mat-icon>delete</mat-icon>
|
|
<span translate>image.delete-image</span>
|
|
</button>
|
|
</mat-menu>
|
|
</div>
|
|
</mat-cell>
|
|
</ng-container>
|
|
<ng-container matColumnDef="imageSelect" stickyEnd>
|
|
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
|
<mat-cell *matCellDef="let image">
|
|
<button mat-stroked-button
|
|
color="primary"
|
|
(click)="selectImage($event, image)">{{ 'action.select' | translate }}</button>
|
|
</mat-cell>
|
|
</ng-container>
|
|
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
|
|
<mat-row [fxShow]="!dataSource.dataLoading"
|
|
[ngClass]="{'mat-row-select': true,
|
|
'mat-selected': dataSource.selection.isSelected(image)}"
|
|
*matRowDef="let image; columns: displayedColumns;" (click)="rowClick($event, image)"></mat-row>
|
|
</table>
|
|
<ng-container *ngIf="(dataSource.isEmpty() | async) && !dataSource.dataLoading">
|
|
<ng-container *ngTemplateOutlet="noImages"></ng-container>
|
|
</ng-container>
|
|
<span [fxShow]="dataSource.dataLoading"
|
|
fxLayoutAlign="center center"
|
|
class="no-data-found">{{ 'common.loading' | translate }}</span>
|
|
</div>
|
|
<mat-divider></mat-divider>
|
|
<mat-paginator [length]="dataSource.total() | async"
|
|
[pageIndex]="pageLink.page"
|
|
[pageSize]="pageLink.pageSize"
|
|
[pageSizeOptions]="pageSizeOptions"
|
|
[hidePageSize]="hidePageSize"
|
|
showFirstLastButtons></mat-paginator>
|
|
</div>
|
|
<div *ngIf="mode === 'grid'" fxFlex [ngClass]="{'mat-padding': !dialogMode}" fxLayout="column">
|
|
<tb-scroll-grid fxFlex
|
|
[columns]="gridColumns"
|
|
[itemSize]="gridImagesItemSizeStrategy"
|
|
[fetchFunction]="gridImagesFetchFunction"
|
|
[filter]="gridImagesFilter"
|
|
[itemCard]="imageCard"
|
|
[loadingCell]="imageLoadingCard"
|
|
[dataLoading]="loadingImages"
|
|
[noData]="noImages">
|
|
</tb-scroll-grid>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ng-template #loadingImages>
|
|
<div fxLayout="column"
|
|
fxLayoutAlign="center center" class="tb-absolute-fill">
|
|
<span>
|
|
{{ 'common.loading' | translate }}
|
|
</span>
|
|
</div>
|
|
</ng-template>
|
|
<ng-template #noImages>
|
|
<div class="tb-no-images">
|
|
<div class="tb-no-data-bg"></div>
|
|
<div class="tb-no-data-text" translate>image.no-images</div>
|
|
</div>
|
|
</ng-template>
|
|
<ng-template #imageCard let-item="item" let-itemIndex="itemIndex">
|
|
<div class="tb-image-card tb-primary-fill">
|
|
<div class="tb-image-card-overlay">
|
|
<div *ngIf="!selectionMode" class="tb-image-card-overlay-buttons">
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ 'image.download-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="downloadImage($event, item)">
|
|
<mat-icon>file_download</mat-icon>
|
|
</button>
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ 'image.export-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="exportImage($event, item)">
|
|
<tb-icon>mdi:file-export</tb-icon>
|
|
</button>
|
|
<button mat-icon-button [disabled]="isLoading$ | async"
|
|
matTooltip="{{ 'image.embed-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="embedImage($event, item, itemIndex)">
|
|
<mat-icon>code</mat-icon>
|
|
</button>
|
|
<button *ngIf="deleteEnabled(item)"
|
|
mat-icon-button [disabled]="(isLoading$ | async)"
|
|
matTooltip="{{ 'image.delete-image' | translate }}"
|
|
matTooltipPosition="above"
|
|
(click)="deleteImage($event, item, itemIndex)">
|
|
<mat-icon>delete</mat-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="tb-image-preview-container">
|
|
<div class="tb-image-preview-overlay" (click)="selectionMode && selectImage($event, item)">
|
|
<button
|
|
*ngIf="!selectionMode"
|
|
mat-flat-button
|
|
color="primary"
|
|
(click)="editImage($event, item, itemIndex)">
|
|
{{ (readonly(item) ? 'image.image-details' : 'image.edit-image') | translate }}
|
|
</button>
|
|
<button
|
|
*ngIf="selectionMode"
|
|
mat-flat-button
|
|
color="primary"
|
|
(click)="selectImage($event, item)">{{ 'action.select' | translate }}
|
|
</button>
|
|
</div>
|
|
<div class="tb-image-preview-spacer"></div>
|
|
<img class="tb-image-preview" [src]="item.link | image: {preview: true} | async" alt="{{ item.title }}">
|
|
</div>
|
|
<div class="tb-image-details">
|
|
<div class="tb-image-title-container">
|
|
<div class="tb-image-title">
|
|
{{ item.title }}
|
|
</div>
|
|
<div *ngIf="isSystem(item)" class="tb-image-sys">sys</div>
|
|
</div>
|
|
<div class="tb-image-info-container">
|
|
<div>{{ item.descriptor.width }}x{{ item.descriptor.height }}</div>
|
|
<mat-divider vertical></mat-divider>
|
|
<div>{{ item.descriptor.size | fileSize }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ng-template>
|
|
<ng-template #imageLoadingCard>
|
|
<div class="tb-image-card tb-primary-fill loading-cell">
|
|
<div class="tb-image-preview-container">
|
|
<div class="tb-image-preview-spacer"></div>
|
|
<div class="tb-image-preview"></div>
|
|
</div>
|
|
<div class="tb-image-details"></div>
|
|
</div>
|
|
</ng-template>
|