parent
7737fce36a
commit
1120d61cf1
@ -0,0 +1,3 @@
|
|||||||
|
<div class="image-wrapper" (click)="fileSelectEvent.emit(this.file)" [class.selected]="this.file.selected">
|
||||||
|
<img [src]="contentUrl">
|
||||||
|
</div>
|
@ -0,0 +1,21 @@
|
|||||||
|
img {
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
height: calc(100% - 20px);
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
background-color: darken(dimgrey, 15);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper.selected {
|
||||||
|
background-color: darken(dimgrey, 5);
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FileGalleryEntryComponent } from './file-gallery-entry.component';
|
||||||
|
|
||||||
|
describe('FileGalleryEntryComponent', () => {
|
||||||
|
let component: FileGalleryEntryComponent;
|
||||||
|
let fixture: ComponentFixture<FileGalleryEntryComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ FileGalleryEntryComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileGalleryEntryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input, OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output, SimpleChanges
|
||||||
|
} from '@angular/core';
|
||||||
|
import {File} from "../../../models/File";
|
||||||
|
import {FileService} from "../../../services/file/file.service";
|
||||||
|
import {SafeResourceUrl} from "@angular/platform-browser";
|
||||||
|
import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service";
|
||||||
|
import {Selectable} from "../../../models/Selectable";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-file-gallery-entry',
|
||||||
|
templateUrl: './file-gallery-entry.component.html',
|
||||||
|
styleUrls: ['./file-gallery-entry.component.scss']
|
||||||
|
})
|
||||||
|
export class FileGalleryEntryComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
|
@Input() file!: Selectable<File>;
|
||||||
|
@Output() fileSelectEvent = new EventEmitter<Selectable<File>>();
|
||||||
|
contentUrl: SafeResourceUrl | undefined;
|
||||||
|
|
||||||
|
private cachedFile: File | undefined;
|
||||||
|
|
||||||
|
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { }
|
||||||
|
|
||||||
|
async ngOnChanges(changes: SimpleChanges): Promise<void> {
|
||||||
|
if (!this.cachedFile || this.file.data.hash !== this.cachedFile.hash) { // handle changes to the file when the component is not destroyed
|
||||||
|
this.cachedFile = this.file.data;
|
||||||
|
await this.loadImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.cachedFile = this.file.data;
|
||||||
|
await this.loadImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadImage() {
|
||||||
|
try {
|
||||||
|
const thumbnails = await this.fileService.getThumbnails(this.file.data.hash);
|
||||||
|
let thumbnail = thumbnails.find(t => (t.height > 250 || t.width > 250) && (t.height < 500 && t.width < 500));
|
||||||
|
thumbnail = thumbnail ?? thumbnails[0];
|
||||||
|
|
||||||
|
if (!thumbnail) {
|
||||||
|
console.log("Thumbnail is empty?!", thumbnails);
|
||||||
|
} else {
|
||||||
|
this.contentUrl = await this.fileService.readThumbnail(thumbnail!!);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.errorBroker.showError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
<div class="gallery-container" fxLayout="column">
|
||||||
|
<button mat-icon-button class="close-button" (click)="this.closeEvent.emit()">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
<div class="file-full-view" fxFlex="80%" (dblclick)="this.selectedFile? this.fileDblClickEvent.emit(this.selectedFile.data) : null">
|
||||||
|
<div class="file-full-view-inner">
|
||||||
|
<img [src]="this.fileContentUrl"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-scroll-view" fxFlex="20%">
|
||||||
|
<cdk-virtual-scroll-viewport #virtualScroll orientation="horizontal" itemSize="250" minBufferPx="1000" maxBufferPx="3000" class="file-scroll-viewport">
|
||||||
|
<div *cdkVirtualFor="let entry of entries" class="file-item">
|
||||||
|
<app-file-gallery-entry [file]="entry" (fileSelectEvent)="onEntrySelect($event)"></app-file-gallery-entry>
|
||||||
|
</div>
|
||||||
|
</cdk-virtual-scroll-viewport>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,52 @@
|
|||||||
|
.file-scroll-viewport {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .file-scroll-viewport > .cdk-virtual-scroll-content-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-file-gallery-entry, .file-item {
|
||||||
|
width: 250px;
|
||||||
|
height: calc(100% - 10px);
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
app-file-gallery-entry {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-full-view {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-full-view-inner {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { FileGalleryComponent } from './file-gallery.component';
|
||||||
|
|
||||||
|
describe('FileGalleryComponent', () => {
|
||||||
|
let component: FileGalleryComponent;
|
||||||
|
let fixture: ComponentFixture<FileGalleryComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ FileGalleryComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FileGalleryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
Component, ElementRef,
|
||||||
|
EventEmitter, HostListener,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnInit,
|
||||||
|
Output, SimpleChanges, ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import {File} from "../../models/File";
|
||||||
|
import {FileService} from "../../services/file/file.service";
|
||||||
|
import {SafeResourceUrl} from "@angular/platform-browser";
|
||||||
|
import {Selectable} from "../../models/Selectable";
|
||||||
|
import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-file-gallery',
|
||||||
|
templateUrl: './file-gallery.component.html',
|
||||||
|
styleUrls: ['./file-gallery.component.scss']
|
||||||
|
})
|
||||||
|
export class FileGalleryComponent implements OnChanges, OnInit {
|
||||||
|
|
||||||
|
@Input() files: File[] = [];
|
||||||
|
@Input() preselectedFile: File | undefined;
|
||||||
|
@Output() fileSelectEvent = new EventEmitter<File | undefined>();
|
||||||
|
@Output() fileDblClickEvent = new EventEmitter<File>();
|
||||||
|
@Output() closeEvent = new EventEmitter<void>();
|
||||||
|
entries: Selectable<File>[] = [];
|
||||||
|
|
||||||
|
@ViewChild("virtualScroll") virtualScroll!: CdkVirtualScrollViewport;
|
||||||
|
|
||||||
|
selectedFile: Selectable<File> | undefined;
|
||||||
|
fileContentUrl: SafeResourceUrl | undefined;
|
||||||
|
|
||||||
|
constructor(private fileService: FileService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new entry is selected
|
||||||
|
* @param {Selectable<File>} entry
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async onEntrySelect(entry: Selectable<File>) {
|
||||||
|
if (entry) {
|
||||||
|
this.selectedFile?.unselect();
|
||||||
|
entry.select();
|
||||||
|
this.selectedFile = entry;
|
||||||
|
await this.loadSelectedFile();
|
||||||
|
this.virtualScroll.scrollToIndex(this.entries.indexOf(entry), "smooth");
|
||||||
|
this.fileSelectEvent.emit(this.selectedFile.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the content url of the selected file
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async loadSelectedFile() {
|
||||||
|
if (this.selectedFile) {
|
||||||
|
this.fileContentUrl = await this.fileService.readFile(this.selectedFile.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
if (!this.selectedFile || this.files.indexOf(this.selectedFile.data) < 0) {
|
||||||
|
await this.onEntrySelect(this.getPreselectedEntry() ?? this.entries[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ngOnChanges(changes: SimpleChanges): Promise<void> {
|
||||||
|
this.entries = this.files.map(f => new Selectable(f, f == this.selectedFile?.data));
|
||||||
|
|
||||||
|
if (!this.selectedFile || this.files.indexOf(this.selectedFile.data) < 0) {
|
||||||
|
await this.onEntrySelect(this.getPreselectedEntry() ?? this.entries[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the previous item in the gallery
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async nextItem() {
|
||||||
|
if (this.selectedFile) {
|
||||||
|
let index = this.entries.indexOf(this.selectedFile) + 1;
|
||||||
|
if (index == this.entries.length) {
|
||||||
|
index--; // restrict to elements
|
||||||
|
}
|
||||||
|
await this.onEntrySelect(this.entries[index]);
|
||||||
|
} else {
|
||||||
|
await this.onEntrySelect(this.entries[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the next item in the gallery
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async previousItem() {
|
||||||
|
if (this.selectedFile) {
|
||||||
|
let index = this.entries.indexOf(this.selectedFile) - 1;
|
||||||
|
if (index < 0) {
|
||||||
|
index++; // restrict to elements
|
||||||
|
}
|
||||||
|
await this.onEntrySelect(this.entries[index]);
|
||||||
|
} else {
|
||||||
|
await this.onEntrySelect(this.entries[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener("window:keydown", ["$event"])
|
||||||
|
private async handleKeydownEvent(event: KeyboardEvent) {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowRight":
|
||||||
|
await this.nextItem();
|
||||||
|
break;
|
||||||
|
case "ArrowLeft":
|
||||||
|
await this.previousItem();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPreselectedEntry(): Selectable<File> | undefined {
|
||||||
|
if (this.preselectedFile) {
|
||||||
|
const entry = this.entries.find(e => e.data.hash == this.preselectedFile?.hash);
|
||||||
|
if (entry) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
export class Selectable<T> {
|
||||||
|
constructor(public data: T, public selected: boolean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public select() {
|
||||||
|
this.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unselect() {
|
||||||
|
this.selected = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggle() {
|
||||||
|
this.selected = !this.selected;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue