Add gallery view

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 7737fce36a
commit 1120d61cf1

@ -11,7 +11,8 @@
color: white color: white
} }
::ng-deep ::-webkit-scrollbar { ::ng-deep ::-webkit-scrollbar {
width: 10px; width: 15px;
height: 15px;
} }
::ng-deep ::-webkit-scrollbar-thumb { ::ng-deep ::-webkit-scrollbar-thumb {

@ -35,6 +35,8 @@ import {MatRippleModule} from "@angular/material/core";
import {FilterDialogComponent} from './components/file-search/filter-dialog/filter-dialog.component'; import {FilterDialogComponent} from './components/file-search/filter-dialog/filter-dialog.component';
import {MatDialogModule} from "@angular/material/dialog"; import {MatDialogModule} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select"; import {MatSelectModule} from "@angular/material/select";
import { FileGalleryComponent } from './components/file-gallery/file-gallery.component';
import { FileGalleryEntryComponent } from './components/file-gallery/file-gallery-entry/file-gallery-entry.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -48,6 +50,8 @@ import {MatSelectModule} from "@angular/material/select";
FileSearchComponent, FileSearchComponent,
SearchPageComponent, SearchPageComponent,
FilterDialogComponent, FilterDialogComponent,
FileGalleryComponent,
FileGalleryEntryComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

@ -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;
}
}

@ -3,7 +3,7 @@ import {
Input, Input,
OnInit, OnInit,
ViewChild, ViewChild,
ElementRef, Output, EventEmitter, OnDestroy ElementRef, Output, EventEmitter, OnDestroy, OnChanges
} from '@angular/core'; } from '@angular/core';
import {File} from "../../../models/File"; import {File} from "../../../models/File";
import {FileService} from "../../../services/file/file.service"; import {FileService} from "../../../services/file/file.service";
@ -18,26 +18,27 @@ import {GridEntry} from "./GridEntry";
templateUrl: './file-grid-entry.component.html', templateUrl: './file-grid-entry.component.html',
styleUrls: ['./file-grid-entry.component.scss'] styleUrls: ['./file-grid-entry.component.scss']
}) })
export class FileGridEntryComponent implements OnInit, OnDestroy { export class FileGridEntryComponent implements OnInit, OnChanges {
@ViewChild("card") card!: ElementRef; @ViewChild("card") card!: ElementRef;
@Input() public gridEntry!: GridEntry; @Input() public gridEntry!: GridEntry;
@Output() clickEvent = new EventEmitter<FileGridEntryComponent>(); @Output() clickEvent = new EventEmitter<FileGridEntryComponent>();
@Output() dblClickEvent = new EventEmitter<FileGridEntryComponent>(); @Output() dblClickEvent = new EventEmitter<FileGridEntryComponent>();
selectedThumbnail: Thumbnail | undefined;
contentUrl: SafeResourceUrl | undefined; contentUrl: SafeResourceUrl | undefined;
private cachedFile: File | undefined;
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { } constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { }
async ngOnInit() { async ngOnInit() {
this.cachedFile = this.gridEntry.file;
await this.loadImage(); await this.loadImage();
} }
public ngOnDestroy(): void { async ngOnChanges() {
if (this.contentUrl) { if (!this.cachedFile || this.gridEntry.file.hash !== this.cachedFile.hash) {
const url = this.contentUrl; this.cachedFile = this.gridEntry.file;
this.contentUrl = undefined; await this.loadImage();
} }
} }
@ -45,7 +46,7 @@ export class FileGridEntryComponent implements OnInit, OnDestroy {
try { try {
const thumbnails = await this.fileService.getThumbnails(this.gridEntry.file.hash); const thumbnails = await this.fileService.getThumbnails(this.gridEntry.file.hash);
let thumbnail = thumbnails.find(t => (t.height > 250 || t.width > 250) && (t.height < 500 && t.width < 500)); let thumbnail = thumbnails.find(t => (t.height > 250 || t.width > 250) && (t.height < 500 && t.width < 500));
this.selectedThumbnail = thumbnail ?? thumbnails[0]; thumbnail = thumbnail ?? thumbnails[0];
if (!thumbnail) { if (!thumbnail) {
console.log("Thumbnail is empty?!", thumbnails); console.log("Thumbnail is empty?!", thumbnails);

@ -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;
}
}

@ -16,9 +16,13 @@
</div> </div>
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<app-file-grid (fileDblClickEvent)="openFile($event)" [files]="files" <app-file-grid *ngIf="!this.showGallery" (fileDblClickEvent)="openGallery($event)" [files]="files"
(fileSelectEvent)="onFileSelect($event)" (fileSelectEvent)="onFileSelect($event)"
(fileMultiselectEvent)="onFileMultiSelect($event)" (fileMultiselectEvent)="onFileMultiSelect($event)"
></app-file-grid> ></app-file-grid>
<app-file-gallery *ngIf="this.showGallery" [files]="files" (fileSelectEvent)="onFileSelect($event)"
(fileDblClickEvent)="openFile($event)" [preselectedFile]="this.preselectedFile"
(closeEvent)="this.showGallery = false"></app-file-gallery>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>

@ -43,6 +43,12 @@ app-file-grid {
padding: 0; padding: 0;
} }
app-file-gallery {
padding: 0;
height: 100%;
width: 100%;
}
.page { .page {
height: 100%; height: 100%;
width: 100%; width: 100%;

@ -19,6 +19,8 @@ export class SearchPageComponent implements OnInit {
tags: Tag[] = []; tags: Tag[] = [];
files: File[] = []; files: File[] = [];
private openingLightbox = false; private openingLightbox = false;
showGallery = false;
preselectedFile: File | undefined;
@ViewChild('filesearch') fileSearch!: FileSearchComponent; @ViewChild('filesearch') fileSearch!: FileSearchComponent;
@ -94,6 +96,11 @@ export class SearchPageComponent implements OnInit {
this.openingLightbox = false; this.openingLightbox = false;
} }
async openGallery(preselectedFile: File) {
this.preselectedFile = preselectedFile;
this.showGallery = true;
}
private async openLightbox(file: File): Promise<void> { private async openLightbox(file: File): Promise<void> {
let url = await this.fileService.readFile(file); let url = await this.fileService.readFile(file);

Loading…
Cancel
Save