diff --git a/mediarepo-ui/src/app/app.component-theme.scss b/mediarepo-ui/src/app/app.component-theme.scss
index 3085e01..79ac3d7 100644
--- a/mediarepo-ui/src/app/app.component-theme.scss
+++ b/mediarepo-ui/src/app/app.component-theme.scss
@@ -11,7 +11,8 @@
color: white
}
::ng-deep ::-webkit-scrollbar {
- width: 10px;
+ width: 15px;
+ height: 15px;
}
::ng-deep ::-webkit-scrollbar-thumb {
diff --git a/mediarepo-ui/src/app/app.module.ts b/mediarepo-ui/src/app/app.module.ts
index 042f63c..f048369 100644
--- a/mediarepo-ui/src/app/app.module.ts
+++ b/mediarepo-ui/src/app/app.module.ts
@@ -35,6 +35,8 @@ import {MatRippleModule} from "@angular/material/core";
import {FilterDialogComponent} from './components/file-search/filter-dialog/filter-dialog.component';
import {MatDialogModule} from "@angular/material/dialog";
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({
declarations: [
@@ -48,6 +50,8 @@ import {MatSelectModule} from "@angular/material/select";
FileSearchComponent,
SearchPageComponent,
FilterDialogComponent,
+ FileGalleryComponent,
+ FileGalleryEntryComponent,
],
imports: [
BrowserModule,
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.html b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.html
new file mode 100644
index 0000000..1d28185
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.scss b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.scss
new file mode 100644
index 0000000..8c1208b
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.scss
@@ -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);
+}
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.spec.ts b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.spec.ts
new file mode 100644
index 0000000..5d0579b
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ FileGalleryEntryComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FileGalleryEntryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.ts b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.ts
new file mode 100644
index 0000000..a666b1e
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery-entry/file-gallery-entry.component.ts
@@ -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;
+ @Output() fileSelectEvent = new EventEmitter>();
+ contentUrl: SafeResourceUrl | undefined;
+
+ private cachedFile: File | undefined;
+
+ constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { }
+
+ async ngOnChanges(changes: SimpleChanges): Promise {
+ 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);
+ }
+ }
+}
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.html b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.html
new file mode 100644
index 0000000..e83642b
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.scss b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.scss
new file mode 100644
index 0000000..77de222
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.scss
@@ -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;
+}
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.spec.ts b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.spec.ts
new file mode 100644
index 0000000..7b8f6e3
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ FileGalleryComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FileGalleryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.ts b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.ts
new file mode 100644
index 0000000..5a433c4
--- /dev/null
+++ b/mediarepo-ui/src/app/components/file-gallery/file-gallery.component.ts
@@ -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();
+ @Output() fileDblClickEvent = new EventEmitter();
+ @Output() closeEvent = new EventEmitter();
+ entries: Selectable[] = [];
+
+ @ViewChild("virtualScroll") virtualScroll!: CdkVirtualScrollViewport;
+
+ selectedFile: Selectable | undefined;
+ fileContentUrl: SafeResourceUrl | undefined;
+
+ constructor(private fileService: FileService) {
+ }
+
+ /**
+ * Called when a new entry is selected
+ * @param {Selectable} entry
+ * @returns {Promise}
+ */
+ async onEntrySelect(entry: Selectable) {
+ 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}
+ */
+ async loadSelectedFile() {
+ if (this.selectedFile) {
+ this.fileContentUrl = await this.fileService.readFile(this.selectedFile.data);
+ }
+ }
+
+ async ngOnInit(): Promise {
+ if (!this.selectedFile || this.files.indexOf(this.selectedFile.data) < 0) {
+ await this.onEntrySelect(this.getPreselectedEntry() ?? this.entries[0])
+ }
+ }
+
+ public async ngOnChanges(changes: SimpleChanges): Promise {
+ 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}
+ */
+ 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}
+ */
+ 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 | undefined {
+ if (this.preselectedFile) {
+ const entry = this.entries.find(e => e.data.hash == this.preselectedFile?.hash);
+ if (entry) {
+ return entry;
+ }
+ }
+ return undefined;
+ }
+}
diff --git a/mediarepo-ui/src/app/components/file-grid/file-grid-entry/file-grid-entry.component.ts b/mediarepo-ui/src/app/components/file-grid/file-grid-entry/file-grid-entry.component.ts
index 76c568c..7dc5b28 100644
--- a/mediarepo-ui/src/app/components/file-grid/file-grid-entry/file-grid-entry.component.ts
+++ b/mediarepo-ui/src/app/components/file-grid/file-grid-entry/file-grid-entry.component.ts
@@ -3,7 +3,7 @@ import {
Input,
OnInit,
ViewChild,
- ElementRef, Output, EventEmitter, OnDestroy
+ ElementRef, Output, EventEmitter, OnDestroy, OnChanges
} from '@angular/core';
import {File} from "../../../models/File";
import {FileService} from "../../../services/file/file.service";
@@ -18,26 +18,27 @@ import {GridEntry} from "./GridEntry";
templateUrl: './file-grid-entry.component.html',
styleUrls: ['./file-grid-entry.component.scss']
})
-export class FileGridEntryComponent implements OnInit, OnDestroy {
+export class FileGridEntryComponent implements OnInit, OnChanges {
@ViewChild("card") card!: ElementRef;
@Input() public gridEntry!: GridEntry;
@Output() clickEvent = new EventEmitter();
@Output() dblClickEvent = new EventEmitter();
- selectedThumbnail: Thumbnail | undefined;
contentUrl: SafeResourceUrl | undefined;
+ private cachedFile: File | undefined;
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { }
async ngOnInit() {
+ this.cachedFile = this.gridEntry.file;
await this.loadImage();
}
- public ngOnDestroy(): void {
- if (this.contentUrl) {
- const url = this.contentUrl;
- this.contentUrl = undefined;
+ async ngOnChanges() {
+ if (!this.cachedFile || this.gridEntry.file.hash !== this.cachedFile.hash) {
+ this.cachedFile = this.gridEntry.file;
+ await this.loadImage();
}
}
@@ -45,7 +46,7 @@ export class FileGridEntryComponent implements OnInit, OnDestroy {
try {
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));
- this.selectedThumbnail = thumbnail ?? thumbnails[0];
+ thumbnail = thumbnail ?? thumbnails[0];
if (!thumbnail) {
console.log("Thumbnail is empty?!", thumbnails);
diff --git a/mediarepo-ui/src/app/models/Selectable.ts b/mediarepo-ui/src/app/models/Selectable.ts
new file mode 100644
index 0000000..f522b18
--- /dev/null
+++ b/mediarepo-ui/src/app/models/Selectable.ts
@@ -0,0 +1,16 @@
+export class Selectable {
+ constructor(public data: T, public selected: boolean) {
+ }
+
+ public select() {
+ this.selected = true;
+ }
+
+ public unselect() {
+ this.selected = false;
+ }
+
+ public toggle() {
+ this.selected = !this.selected;
+ }
+}
diff --git a/mediarepo-ui/src/app/pages/home/search-page/search-page.component.html b/mediarepo-ui/src/app/pages/home/search-page/search-page.component.html
index 3359c08..f24ef21 100644
--- a/mediarepo-ui/src/app/pages/home/search-page/search-page.component.html
+++ b/mediarepo-ui/src/app/pages/home/search-page/search-page.component.html
@@ -16,9 +16,13 @@
-
+
+
diff --git a/mediarepo-ui/src/app/pages/home/search-page/search-page.component.scss b/mediarepo-ui/src/app/pages/home/search-page/search-page.component.scss
index 179cb35..1e0f5c4 100644
--- a/mediarepo-ui/src/app/pages/home/search-page/search-page.component.scss
+++ b/mediarepo-ui/src/app/pages/home/search-page/search-page.component.scss
@@ -43,6 +43,12 @@ app-file-grid {
padding: 0;
}
+app-file-gallery {
+ padding: 0;
+ height: 100%;
+ width: 100%;
+}
+
.page {
height: 100%;
width: 100%;
diff --git a/mediarepo-ui/src/app/pages/home/search-page/search-page.component.ts b/mediarepo-ui/src/app/pages/home/search-page/search-page.component.ts
index 93f2988..4c746e8 100644
--- a/mediarepo-ui/src/app/pages/home/search-page/search-page.component.ts
+++ b/mediarepo-ui/src/app/pages/home/search-page/search-page.component.ts
@@ -19,6 +19,8 @@ export class SearchPageComponent implements OnInit {
tags: Tag[] = [];
files: File[] = [];
private openingLightbox = false;
+ showGallery = false;
+ preselectedFile: File | undefined;
@ViewChild('filesearch') fileSearch!: FileSearchComponent;
@@ -94,6 +96,11 @@ export class SearchPageComponent implements OnInit {
this.openingLightbox = false;
}
+ async openGallery(preselectedFile: File) {
+ this.preselectedFile = preselectedFile;
+ this.showGallery = true;
+ }
+
private async openLightbox(file: File): Promise {
let url = await this.fileService.readFile(file);