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