Cleanup code

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 2691653274
commit e6026cfd27

@ -30,6 +30,7 @@ describe('AppComponent', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('mediarepo-ui app is running!');
expect(compiled.querySelector('.content span')?.textContent)
.toContain('mediarepo-ui app is running!');
});
});

@ -21,7 +21,8 @@ export class AppComponent implements OnInit{
}
async ngOnInit() {
this.errorBroker.errorCb = (err: { message: string }) => this.showError(err);
this.errorBroker.errorCb = (err: { message: string }) => this.showError(
err);
this.errorBroker.infoCb = (info: string) => this.showInfo(info);
await this.repoService.loadRepositories();
}

@ -4,7 +4,7 @@
<div mat-dialog-content>
{{message}}
</div>
<div mat-dialog-actions class="confirm-dialog-actions">
<button mat-stroked-button [color]="this.denyColor" (click)="closeDialog(false)">{{denyAction}}</button>
<button mat-flat-button [color]="this.confirmColor" (click)="closeDialog(true)">{{confirmAction}}</button>
<div class="confirm-dialog-actions" mat-dialog-actions>
<button (click)="closeDialog(false)" [color]="this.denyColor" mat-stroked-button>{{denyAction}}</button>
<button (click)="closeDialog(true)" [color]="this.confirmColor" mat-flat-button>{{confirmAction}}</button>
</div>

@ -1,4 +1,4 @@
import {Component, Inject, OnInit} from '@angular/core';
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ThemePalette} from "@angular/material/core";

@ -1,5 +1,5 @@
<div #imageContainer class="image-container" (window:resize)="this.adjustSize(image, imageContainer)">
<img #image [src]="this.imageSrc" alt="" (load)="this.adjustSize(image, imageContainer)"
[class.scale-width]="scaleWidth && maximizeWidth" [class.scale-height]="(!scaleWidth) && maximizeHeight"
<div #imageContainer (window:resize)="this.adjustSize(image, imageContainer)" class="image-container">
<img #image (load)="this.adjustSize(image, imageContainer)" [class.scale-height]="(!scaleWidth) && maximizeHeight" [class.scale-width]="scaleWidth && maximizeWidth"
[src]="this.imageSrc" alt=""
decoding="async">
</div>

@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, Input} from '@angular/core';
import {SafeResourceUrl} from "@angular/platform-browser";
@Component({
@ -14,7 +14,8 @@ export class ContentAwareImageComponent {
scaleWidth = false;
constructor() { }
constructor() {
}
/**
* Fits the image into the container

@ -1,4 +1,4 @@
<div class="image-wrapper" (click)="fileSelectEvent.emit(this.file)" [class.selected]="this.file.selected">
<div (click)="fileSelectEvent.emit(this.file)" [class.selected]="this.file.selected" class="image-wrapper">
<mat-progress-spinner *ngIf="!contentUrl"></mat-progress-spinner>
<app-content-aware-image *ngIf="contentUrl" [imageSrc]="contentUrl"></app-content-aware-image>
</div>

@ -1,10 +1,11 @@
import {
Component,
EventEmitter,
Input, OnChanges,
OnDestroy,
Input,
OnChanges,
OnInit,
Output, SimpleChanges
Output,
SimpleChanges
} from '@angular/core';
import {File} from "../../../models/File";
import {FileService} from "../../../services/file/file.service";
@ -25,7 +26,8 @@ export class FileGalleryEntryComponent implements OnInit, OnChanges {
private cachedFile: File | undefined;
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { }
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) {
}
async ngOnChanges(changes: SimpleChanges): Promise<void> {
if (changes["file"] && (!this.cachedFile || this.file.data.hash !== this.cachedFile!.hash)) { // handle changes to the file when the component is not destroyed
@ -46,7 +48,8 @@ export class FileGalleryEntryComponent implements OnInit, OnChanges {
try {
const hash = this.file.data.hash;
const thumbnails = await this.fileService.getThumbnails(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));
thumbnail = thumbnail ?? thumbnails[0];
if (!thumbnail) {

@ -1,22 +1,24 @@
<div class="gallery-container" fxLayout="column">
<button mat-icon-button class="close-button" (click)="this.closeEvent.emit(this)">
<button (click)="this.closeEvent.emit(this)" class="close-button" mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<div class="file-full-view" fxFlex="80%"
(dblclick)="this.selectedFile? this.fileDblClickEvent.emit(this.selectedFile.data) : null">
<div (mouseenter)="this.mouseInImageView = true" (mouseleave)="this.mouseInImageView = false" class="file-full-view-inner">
<div (dblclick)="this.selectedFile? this.fileDblClickEvent.emit(this.selectedFile.data) : null" class="file-full-view"
fxFlex="80%">
<div (mouseenter)="this.mouseInImageView = true" (mouseleave)="this.mouseInImageView = false"
class="file-full-view-inner">
<div *ngIf="!this.fileContentUrl" class="url-loading-backdrop">
<mat-progress-spinner mode="indeterminate" color="primary"></mat-progress-spinner>
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
<div class="zoom-slider">
<mat-slider min="0.5" max="4" [value]="this.imageZoom"
(input)="this.imageZoom=$event.value ?? 1" vertical step="0.1"></mat-slider>
<button mat-icon-button (click)="this.resetImage()">
<mat-slider (input)="this.imageZoom=$event.value ?? 1" [value]="this.imageZoom" max="4"
min="0.5" step="0.1" vertical></mat-slider>
<button (click)="this.resetImage()" mat-icon-button>
<mat-icon>refresh</mat-icon>
</button>
</div>
<div #imageDragContainer [cdkDragFreeDragPosition]="this.imagePosition" (cdkDragMoved)="this.onDragMoved($event)" *ngIf="this.fileContentUrl" cdkDrag class="image-drag-container">
<div #scaledImage class="image-scale-container" [style]="{scale: this.imageZoom}">
<div #imageDragContainer (cdkDragMoved)="this.onDragMoved($event)" *ngIf="this.fileContentUrl"
[cdkDragFreeDragPosition]="this.imagePosition" cdkDrag class="image-drag-container">
<div #scaledImage [style]="{scale: this.imageZoom}" class="image-scale-container">
<app-content-aware-image [imageSrc]="this.fileContentUrl"></app-content-aware-image>
</div>
</div>
@ -24,10 +26,10 @@
</div>
<mat-divider fxFlex></mat-divider>
<div class="file-scroll-view" fxFlex="20%">
<cdk-virtual-scroll-viewport #virtualScroll orientation="horizontal" itemSize="260" minBufferPx="1000"
maxBufferPx="3000" class="file-scroll-viewport">
<cdk-virtual-scroll-viewport #virtualScroll class="file-scroll-viewport" itemSize="260" maxBufferPx="3000"
minBufferPx="1000" orientation="horizontal">
<div *cdkVirtualFor="let entry of entries" class="file-item">
<app-file-gallery-entry [file]="entry" (fileSelectEvent)="onEntrySelect($event)"></app-file-gallery-entry>
<app-file-gallery-entry (fileSelectEvent)="onEntrySelect($event)" [file]="entry"></app-file-gallery-entry>
</div>
</cdk-virtual-scroll-viewport>
</div>

@ -61,6 +61,7 @@ app-content-aware-image {
width: 100%;
overflow: hidden;
display: flex;
mat-progress-spinner {
margin: auto;
}

@ -1,17 +1,21 @@
import {
Component, ElementRef,
EventEmitter, HostListener,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
OnChanges,
OnInit,
Output, SimpleChanges, ViewChild
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";
import {CdkDrag, CdkDragMove, DragRef, Point} from "@angular/cdk/drag-drop";
import {CdkDragMove} from "@angular/cdk/drag-drop";
@Component({
selector: 'app-file-gallery',
@ -29,7 +33,8 @@ export class FileGalleryComponent implements OnChanges, OnInit {
@ViewChild("virtualScroll") virtualScroll!: CdkVirtualScrollViewport;
@ViewChild("scaledImage") scaledImage: ElementRef<HTMLDivElement> | undefined;
@ViewChild("imageDragContainer") imageDragContainer: ElementRef<HTMLDivElement> | undefined;
@ViewChild(
"imageDragContainer") imageDragContainer: ElementRef<HTMLDivElement> | undefined;
public selectedFile: Selectable<File> | undefined;
public fileContentUrl: SafeResourceUrl | undefined;
@ -58,10 +63,12 @@ export class FileGalleryComponent implements OnChanges, OnInit {
if (this.virtualScroll) {
const viewportSize = this.virtualScroll.getViewportSize();
const indexAdjustment = (viewportSize / 260) / 2; // adjustment to have the selected item centered
this.virtualScroll.scrollToIndex(Math.max(selectedIndex - indexAdjustment, 0), "smooth");
this.virtualScroll.scrollToIndex(
Math.max(selectedIndex - indexAdjustment, 0), "smooth");
if (selectedIndex > indexAdjustment) {
this.virtualScroll.scrollToOffset(this.virtualScroll.measureScrollOffset("left") + 130, "smooth");
this.virtualScroll.scrollToOffset(
this.virtualScroll.measureScrollOffset("left") + 130, "smooth");
}
}
this.fileSelectEvent.emit(this.selectedFile.data);
@ -136,6 +143,11 @@ export class FileGalleryComponent implements OnChanges, OnInit {
this.imagePosition = {x: 0, y: 0};
}
public onDragMoved($event: CdkDragMove<HTMLDivElement>): void {
this.imagePosition.x += $event.delta.x;
this.imagePosition.y += $event.delta.y;
}
@HostListener("window:keydown", ["$event"])
private async handleKeydownEvent(event: KeyboardEvent) {
switch (event.key) {
@ -180,9 +192,4 @@ export class FileGalleryComponent implements OnChanges, OnInit {
}
return undefined;
}
public onDragMoved($event: CdkDragMove<HTMLDivElement>): void {
this.imagePosition.x += $event.delta.x;
this.imagePosition.y += $event.delta.y;
}
}

@ -1,7 +1,8 @@
<mat-card #card (click)="clickEvent.emit(this)" (dblclick)="dblClickEvent.emit(this)" [ngClass]="{'selected': gridEntry.selected}">
<mat-card #card (click)="clickEvent.emit(this)" (dblclick)="dblClickEvent.emit(this)"
[ngClass]="{'selected': gridEntry.selected}">
<mat-card-title *ngIf="!!gridEntry.file?.name">{{gridEntry.file?.name}}</mat-card-title>
<mat-card-content *ngIf="contentUrl !== undefined">
<app-content-aware-image class="entry-image" *ngIf="contentUrl" [imageSrc]="contentUrl"></app-content-aware-image>
<app-content-aware-image *ngIf="contentUrl" [imageSrc]="contentUrl" class="entry-image"></app-content-aware-image>
</mat-card-content>
<mat-card-footer>
<mat-progress-bar *ngIf="contentUrl === undefined" mode="indeterminate"></mat-progress-bar>

@ -1,16 +1,17 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
ViewChild,
ElementRef, Output, EventEmitter, OnDestroy, OnChanges
Output,
ViewChild
} from '@angular/core';
import {File} from "../../../models/File";
import {FileService} from "../../../services/file/file.service";
import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service";
import {SafeResourceUrl} from "@angular/platform-browser";
import {MatCard} from "@angular/material/card";
import {Thumbnail} from "../../../models/Thumbnail";
import {GridEntry} from "./GridEntry";
@Component({
@ -28,7 +29,8 @@ export class FileGridEntryComponent implements OnInit, OnChanges {
contentUrl: SafeResourceUrl | undefined;
private cachedFile: File | undefined;
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { }
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) {
}
async ngOnInit() {
this.cachedFile = this.gridEntry.file;
@ -44,14 +46,17 @@ export class FileGridEntryComponent implements OnInit, OnChanges {
async loadImage() {
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));
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));
thumbnail = thumbnail ?? thumbnails[0];
if (!thumbnail) {
console.log("Thumbnail is empty?!", thumbnails);
} else {
this.contentUrl = await this.fileService.readThumbnail(thumbnail!!);
this.contentUrl = await this.fileService.readThumbnail(
thumbnail!!);
}
} catch (err) {
this.errorBroker.showError(err);

@ -1,8 +1,10 @@
<div class="file-gallery-inner">
<cdk-virtual-scroll-viewport #virtualScrollGrid class="file-scroll" maxBufferPx="500" minBufferPx="500" itemSize="250">
<cdk-virtual-scroll-viewport #virtualScrollGrid class="file-scroll" itemSize="250" maxBufferPx="500"
minBufferPx="500">
<div *cdkVirtualFor="let rowEntry of partitionedGridEntries">
<div class="file-row">
<app-file-grid-entry (clickEvent)="setSelectedFile($event.gridEntry)" (dblClickEvent)="fileDblClickEvent.emit($event.gridEntry.file)"
<app-file-grid-entry (clickEvent)="setSelectedFile($event.gridEntry)"
(dblClickEvent)="fileDblClickEvent.emit($event.gridEntry.file)"
*ngFor="let gridEntry of rowEntry" [gridEntry]="gridEntry"></app-file-grid-entry>
</div>
</div>

@ -1,13 +1,16 @@
import {
Component, ElementRef,
Component,
ElementRef,
EventEmitter,
HostListener,
Input, OnChanges,
Input,
OnChanges,
OnInit,
Output, QueryList, SimpleChanges, ViewChild, ViewChildren
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import {File} from "../../models/File";
import {FileService} from "../../services/file/file.service";
import {FileGridEntryComponent} from "./file-grid-entry/file-grid-entry.component";
import {GridEntry} from "./file-grid-entry/GridEntry";
import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
@ -30,11 +33,10 @@ export class FileGridComponent implements OnChanges, OnInit {
@ViewChild("galleryWrapper") galleryWrapper!: ElementRef<HTMLDivElement>;
selectedEntries: GridEntry[] = [];
partitionedGridEntries: GridEntry[][] = [];
private shiftClicked = false;
private ctrlClicked = false;
private gridEntries: GridEntry[] = []
partitionedGridEntries: GridEntry[][] = [];
constructor() {
}

@ -1,25 +1,25 @@
<div class="tag-input-list-and-actions">
<div class="tag-input-list" #tagInputList>
<div #tagInputList class="tag-input-list">
<div class="tag-input-list-inner">
<div class="tag-input-item" *ngFor="let tag of searchTags" mat-ripple
(click)="removeSearchTag(tag)">{{tag.getNormalizedTag()}}</div>
<div (click)="removeSearchTag(tag)" *ngFor="let tag of searchTags" class="tag-input-item"
mat-ripple>{{tag.getNormalizedTag()}}</div>
</div>
</div>
<button id="delete-all-tags-button" mat-icon-button (click)="removeAllSearchTags()">
<button (click)="removeAllSearchTags()" id="delete-all-tags-button" mat-icon-button>
<mat-icon>delete-sweep</mat-icon>
</button>
</div>
<mat-form-field class="full-width" appearance="fill">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Enter tags to filter for</mat-label>
<input matInput
#tagInput
<input #tagInput
(keydown)="addSearchTagByInput($event)"
[formControl]="formControl"
[matAutocomplete]="auto"
[formControl]="formControl"/>
matInput/>
<mat-autocomplete #auto (optionSelected)="addSearchTagByAutocomplete($event)">
<mat-option *ngFor="let tag of suggestionTags | async" [value]="tag">
{{tag}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<button mat-flat-button id="sort-button" (click)="openSortDialog()">Sort: {{sortExpression.join(", ")}}</button>
<button (click)="openSortDialog()" id="sort-button" mat-flat-button>Sort: {{sortExpression.join(", ")}}</button>

@ -1,7 +1,11 @@
import {
AfterViewChecked,
Component,
ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild
} from '@angular/core';
import {TagService} from "../../services/tag/tag.service";
@ -51,17 +55,6 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
this.inputList.nativeElement.scrollLeft = this.inputList.nativeElement.scrollWidth;
}
private filterSuggestionTag(tag: string) {
const negated = tag.startsWith("-");
const normalizedTag = tag.replace(/^-/, "");
return this.validTags.filter(
t => t.includes(normalizedTag) && this.searchTags.findIndex(
s => s.name === t) < 0)
.map(t => negated ? "-" + t : t)
.slice(0, 20);
}
public async searchForFiles() {
this.searchStartEvent.emit();
try {
@ -135,4 +128,15 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
}
});
}
private filterSuggestionTag(tag: string) {
const negated = tag.startsWith("-");
const normalizedTag = tag.replace(/^-/, "");
return this.validTags.filter(
t => t.includes(normalizedTag) && this.searchTags.findIndex(
s => s.name === t) < 0)
.map(t => negated ? "-" + t : t)
.slice(0, 20);
}
}

@ -1,12 +1,14 @@
<h1 mat-dialog-title>Sort Entries</h1>
<div mat-dialog-content>
<div cdkDropList class="sort-input-list" (cdkDropListDropped)="this.onSortEntryDrop($event)">
<div cdkDrag class="sort-input-row" *ngFor="let sortKey of sortEntries">
<div class="drag-placeholder" *cdkDragPlaceholder></div>
<div class="drag-handle" cdkDragHandle><mat-icon>drag_handle</mat-icon></div>
<div (cdkDropListDropped)="this.onSortEntryDrop($event)" cdkDropList class="sort-input-list">
<div *ngFor="let sortKey of sortEntries" cdkDrag class="sort-input-row">
<div *cdkDragPlaceholder class="drag-placeholder"></div>
<div cdkDragHandle class="drag-handle">
<mat-icon>drag_handle</mat-icon>
</div>
<mat-form-field>
<mat-label>Key</mat-label>
<mat-select required [(value)]="sortKey.sortType">
<mat-select [(value)]="sortKey.sortType" required>
<mat-option value="Namespace">Namespace</mat-option>
<mat-option value="FileName">File Name</mat-option>
<mat-option value="FileSize">File Size</mat-option>
@ -19,9 +21,10 @@
</mat-form-field>
<mat-form-field *ngIf="sortKey.sortType === 'Namespace'">
<mat-label>Namespace Name</mat-label>
<input #namespaceInput matInput required [value]="sortKey.namespaceName ?? ''" (change)="sortKey.namespaceName = namespaceInput.value">
<input #namespaceInput (change)="sortKey.namespaceName = namespaceInput.value" [value]="sortKey.namespaceName ?? ''" matInput
required>
</mat-form-field>
<div class="filler" *ngIf="sortKey.sortType !== 'Namespace'"></div>
<div *ngIf="sortKey.sortType !== 'Namespace'" class="filler"></div>
<mat-form-field>
<mat-label>Direction</mat-label>
<mat-select [(value)]="sortKey.sortDirection" required>
@ -29,12 +32,17 @@
<mat-option value="Descending">Descending</mat-option>
</mat-select>
</mat-form-field>
<button *ngIf="sortEntries.indexOf(sortKey) === sortEntries.length -1" mat-flat-button (click)="addNewSortKey()"><mat-icon>add</mat-icon></button>
<button *ngIf="sortEntries.indexOf(sortKey) !== sortEntries.length -1" mat-flat-button (click)="removeSortKey(sortKey)"><mat-icon>remove</mat-icon></button>
<button (click)="addNewSortKey()" *ngIf="sortEntries.indexOf(sortKey) === sortEntries.length -1" mat-flat-button>
<mat-icon>add</mat-icon>
</button>
<button (click)="removeSortKey(sortKey)" *ngIf="sortEntries.indexOf(sortKey) !== sortEntries.length -1"
mat-flat-button>
<mat-icon>remove</mat-icon>
</button>
</div>
</div>
</div>
<div class="dialog-actions" mat-dialog-actions>
<button mat-flat-button color="primary" (click)="confirmSort()">Sort</button>
<button mat-stroked-button color="accent" (click)="cancelSort()">Cancel</button>
<button (click)="confirmSort()" color="primary" mat-flat-button>Sort</button>
<button (click)="cancelSort()" color="accent" mat-stroked-button>Cancel</button>
</div>

@ -21,6 +21,7 @@ mat-form-field, .filler, .drag-handle {
display: flex;
flex-direction: row-reverse;
width: 100%;
button {
margin-left: 1em;
}

@ -12,7 +12,8 @@ export class FilterDialogComponent {
public sortEntries: SortKey[] = []
constructor(public dialogRef: MatDialogRef<FilterDialogComponent>, @Inject(MAT_DIALOG_DATA) data: any) {
constructor(public dialogRef: MatDialogRef<FilterDialogComponent>, @Inject(
MAT_DIALOG_DATA) data: any) {
this.sortEntries = data.sortEntries;
}

@ -1,4 +1,4 @@
<div class="tag-item-wrapper">
<span *ngIf="tag.namespace" class="tag-item-namespace" [style]="{color: namespaceColor}">{{tag.namespace}}:</span>
<span class="tag-item-name" [style]="{color: tagColor}">{{tag.name}}</span>
<span *ngIf="tag.namespace" [style]="{color: namespaceColor}" class="tag-item-namespace">{{tag.namespace}}:</span>
<span [style]="{color: tagColor}" class="tag-item-name">{{tag.name}}</span>
</div>

@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core';
import {Component, Input} from '@angular/core';
import {Tag} from "../../models/Tag";
@Component({
@ -12,5 +12,6 @@ export class TagItemComponent {
@Input() namespaceColor: string | undefined;
@Input() tagColor: string | undefined;
constructor() { }
constructor() {
}
}

@ -9,5 +9,6 @@ export class File {
public creation_time: Date,
public change_time: Date,
public import_time: Date,
) {}
) {
}
}

@ -4,5 +4,6 @@ export class Repository {
public address: string | undefined,
public path: string | undefined,
public local: boolean,
) {}
) {
}
}

@ -5,7 +5,8 @@ export class SortKey {
public sortType: "Namespace" | "FileName" | "FileSize" | "FileImportedTime" | "FileCreatedTime" | "FileChangeTime" | "FileType" | "NumTags",
public sortDirection: "Ascending" | "Descending",
public namespaceName: string | undefined
) {}
) {
}
public toString(): string {
if (this.sortType == "Namespace") {
@ -18,7 +19,12 @@ export class SortKey {
public toBackendType(): any {
if (this.sortType == "Namespace") {
return {"Namespace": {direction: this.sortDirection, name: this.namespaceName}}
return {
"Namespace": {
direction: this.sortDirection,
name: this.namespaceName
}
}
} else {
let returnObj: any = {};
returnObj[this.sortType] = this.sortDirection;

@ -6,7 +6,8 @@ export class Tag {
public id: number,
public name: string,
public namespace: string | undefined
) {}
) {
}
public getNormalizedOutput(): string {
if (!this.normalizedTag) {
@ -14,4 +15,4 @@ export class Tag {
}
return this.normalizedTag;
}
};
}

@ -3,8 +3,9 @@
<mat-tab label="Search">
<div class="search-tab-inner" fxLayout="column">
<div id="file-search-input">
<app-file-search #filesearch (searchStartEvent)="this.searchStartEvent.emit()"
(searchEndEvent)="this.searchEndEvent.emit()" [validTags]="this.getValidTagsForSearch()"></app-file-search>
<app-file-search #filesearch (searchEndEvent)="this.searchEndEvent.emit()"
(searchStartEvent)="this.searchStartEvent.emit()"
[validTags]="this.getValidTagsForSearch()"></app-file-search>
</div>
<mat-divider fxFlex="1em"></mat-divider>
<div class="tag-list-header" fxFlex="40px">
@ -13,7 +14,7 @@
</div>
<div class="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill>
<cdk-virtual-scroll-viewport itemSize="50" maxBufferPx="4000" minBufferPx="500">
<div class="selectable-tag" *cdkVirtualFor="let tag of tags" matRipple (click)="addSearchTag(tag)">
<div (click)="addSearchTag(tag)" *cdkVirtualFor="let tag of tags" class="selectable-tag" matRipple>
<app-tag-item [tag]="tag"></app-tag-item>
</div>
</cdk-virtual-scroll-viewport>

@ -1,16 +1,17 @@
import {
Component,
EventEmitter,
Input, OnChanges,
Input,
OnChanges,
OnInit,
Output, SimpleChanges,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import {Tag} from "../../../../models/Tag";
import {TagService} from "../../../../services/tag/tag.service";
import {FileService} from "../../../../services/file/file.service";
import {File} from "../../../../models/File";
import {MatSelectionListChange} from "@angular/material/list";
import {FileSearchComponent} from "../../../../components/file-search/file-search.component";
import {RepositoryService} from "../../../../services/repository/repository.service";

@ -1,19 +1,20 @@
<mat-drawer-container class="page">
<mat-drawer mode="side" opened disableClose>
<mat-drawer disableClose mode="side" opened>
<app-files-tab-sidebar (searchEndEvent)="this.contentLoading = false"
(searchStartEvent)="this.contentLoading = true" [selectedFiles]="this.selectedFiles"></app-files-tab-sidebar>
(searchStartEvent)="this.contentLoading = true"
[selectedFiles]="this.selectedFiles"></app-files-tab-sidebar>
</mat-drawer>
<mat-drawer-content>
<div *ngIf="contentLoading" class="spinner-overlay">
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
</div>
<app-file-grid *ngIf="!this.showGallery" (fileDblClickEvent)="openGallery($event)" [files]="files"
(fileSelectEvent)="onFileSelect($event)"
<app-file-grid (fileDblClickEvent)="openGallery($event)" (fileMultiselectEvent)="onFileMultiSelect($event)" (fileSelectEvent)="onFileSelect($event)"
*ngIf="!this.showGallery"
[files]="files"
[preselectedFile]="this.preselectedFile"
(fileMultiselectEvent)="onFileMultiSelect($event)"
></app-file-grid>
<app-file-gallery *ngIf="this.showGallery" [files]="files" (fileSelectEvent)="onFileSelect($event)"
[preselectedFile]="this.preselectedFile"
(closeEvent)="this.closeGallery($event.selectedFile?.data)"></app-file-gallery>
<app-file-gallery (closeEvent)="this.closeGallery($event.selectedFile?.data)" (fileSelectEvent)="onFileSelect($event)" *ngIf="this.showGallery"
[files]="files"
[preselectedFile]="this.preselectedFile"></app-file-gallery>
</mat-drawer-content>
</mat-drawer-container>

@ -1,13 +1,7 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {Tag} from "../../../models/Tag";
import {Component, OnInit} from '@angular/core';
import {File} from "../../../models/File";
import {FileSearchComponent} from "../../../components/file-search/file-search.component";
import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service";
import {FileService} from "../../../services/file/file.service";
import {TagService} from "../../../services/tag/tag.service";
import {Lightbox, LIGHTBOX_EVENT, LightboxEvent} from "ngx-lightbox";
import {MatSelectionListChange} from "@angular/material/list";
import {SortKey} from "../../../models/SortKey";
import {RepositoryService} from "../../../services/repository/repository.service";
@Component({

@ -16,7 +16,8 @@ export class HomeComponent implements OnInit {
@ViewChild("tabGroup") tabGroup!: MatTabGroup;
constructor(private repoService: RepositoryService, private tagService: TagService) {}
constructor(private repoService: RepositoryService, private tagService: TagService) {
}
public async ngOnInit(): Promise<void> {
this.selectedRepository = this.repoService.selectedRepository.getValue();

@ -2,35 +2,35 @@
<div mat-dialog-content>
<form [formGroup]="formGroup">
<mat-form-field matTooltipShowDelay="1000" matTooltip="A unique name for the repository">
<mat-form-field matTooltip="A unique name for the repository" matTooltipShowDelay="1000">
<mat-label>Name</mat-label>
<input matInput formControlName="name" (change)="validateName()" (input)="validateName()" (blur)="validateName()">
<input (blur)="validateName()" (change)="validateName()" (input)="validateName()" formControlName="name" matInput>
</mat-form-field>
<mat-form-field matTooltipShowDelay="1000"
matTooltip="Type of repository if it's on the local system or somewhere else">
<mat-form-field matTooltip="Type of repository if it's on the local system or somewhere else"
matTooltipShowDelay="1000">
<mat-label>Type</mat-label>
<mat-select #repoTypeSelect formControlName="repositoryType"
(selectionChange)="onTypeChange(repoTypeSelect.value)">
<mat-select #repoTypeSelect (selectionChange)="onTypeChange(repoTypeSelect.value)"
formControlName="repositoryType">
<mat-option value="local">Local</mat-option>
<mat-option value="remote">Remote</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="repoTypeSelect.value === 'local'"
matTooltipShowDelay="1000"
matTooltip="Path where the repository is located or should be created">
<button class="button-folder-select" mat-button (click)="openFolderDialog()">
matTooltip="Path where the repository is located or should be created"
matTooltipShowDelay="1000">
<button (click)="openFolderDialog()" class="button-folder-select" mat-button>
<mat-icon>folder</mat-icon>
</button>
<mat-label>Path</mat-label>
<input matInput formControlName="path" (change)="this.checkLocalRepoExists()">
<input (change)="this.checkLocalRepoExists()" formControlName="path" matInput>
</mat-form-field>
<mat-form-field *ngIf="repoTypeSelect.value === 'remote'" matTooltipShowDelay="1000"
matTooltip="IP address and port of the remote repository">
<mat-form-field *ngIf="repoTypeSelect.value === 'remote'" matTooltip="IP address and port of the remote repository"
matTooltipShowDelay="1000">
<mat-label>Address</mat-label>
<input matInput formControlName="address">
<input formControlName="address" matInput>
</mat-form-field>
</form>
<div *ngIf="repoTypeSelect.value === 'remote'" class="connection-state">
@ -38,17 +38,17 @@
</div>
</div>
<div class="dialog-buttons" mat-dialog-actions>
<button mat-stroked-button color="accent" (click)="closeDialog()">Cancel</button>
<button mat-flat-button *ngIf="repoTypeSelect.value === 'remote' || this.localRepoExists"
[disabled]="!formGroup.valid" matTooltip="Add the existing repository" color="primary"
(click)="addRepository()">Add
<button (click)="closeDialog()" color="accent" mat-stroked-button>Cancel</button>
<button (click)="addRepository()" *ngIf="repoTypeSelect.value === 'remote' || this.localRepoExists"
[disabled]="!formGroup.valid" color="primary" mat-flat-button
matTooltip="Add the existing repository">Add
</button>
<button mat-flat-button *ngIf="repoTypeSelect.value === 'local' && !this.localRepoExists"
<button (click)="this.initLocalRepository()" *ngIf="repoTypeSelect.value === 'local' && !this.localRepoExists"
[disabled]="!formGroup.valid"
matTooltip="Initialize the repository in the specified path" color="accent"
(click)="this.initLocalRepository()">Init
color="accent" mat-flat-button
matTooltip="Initialize the repository in the specified path">Init
</button>
<button class="check-connection-button" *ngIf="repoTypeSelect.value === 'remote'" [disabled]="!formGroup.valid"
mat-stroked-button (click)="checkRepositoryStatus()">Check Connection
<button (click)="checkRepositoryStatus()" *ngIf="repoTypeSelect.value === 'remote'" [disabled]="!formGroup.valid"
class="check-connection-button" mat-stroked-button>Check Connection
</button>
</div>

@ -2,7 +2,8 @@ import {Component, Inject, OnInit} from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup, ValidationErrors,
FormGroup,
ValidationErrors,
Validators
} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
@ -37,7 +38,8 @@ export class AddRepositoryDialogComponent implements OnInit {
}
ngOnInit(): void {
this.repoService.repositories.subscribe(repositories => this.repositories = repositories)
this.repoService.repositories.subscribe(
repositories => this.repositories = repositories)
}
public async checkRepositoryStatus() {
@ -49,7 +51,8 @@ export class AddRepositoryDialogComponent implements OnInit {
}
public async checkLocalRepoExists() {
this.localRepoExists = await this.repoService.checkLocalRepositoryExists(this.formGroup.value.path);
this.localRepoExists = await this.repoService.checkLocalRepositoryExists(
this.formGroup.value.path);
}
public async initLocalRepository() {
@ -63,7 +66,8 @@ export class AddRepositoryDialogComponent implements OnInit {
path = repositoryType === "local" ? path : undefined;
address = repositoryType === "remote" ? address : undefined;
try {
await this.repoService.addRepository(name, path, address, repositoryType === "local");
await this.repoService.addRepository(name, path, address,
repositoryType === "local");
this.dialogRef.close();
} catch (err) {
this.errorBroker.showError(err);
@ -114,7 +118,8 @@ export class AddRepositoryDialogComponent implements OnInit {
}
validatePath(control: AbstractControl): ValidationErrors | null {
const repositoryType = control.parent?.get("repositoryType")?.value ?? "local";
const repositoryType = control.parent?.get(
"repositoryType")?.value ?? "local";
if (repositoryType === "local") {
return control.value.length > 0 ? null : {valueRequired: control.value};
@ -123,7 +128,8 @@ export class AddRepositoryDialogComponent implements OnInit {
}
validateAddress(control: AbstractControl): ValidationErrors | null {
const repositoryType = control.parent?.get("repositoryType")?.value ?? "remote";
const repositoryType = control.parent?.get(
"repositoryType")?.value ?? "remote";
if (repositoryType === "remote") {
const match = /(\d+\.){3}\d+:\d+|\S+:\d+/.test(control.value)

@ -1,6 +1,6 @@
<div class="repo-page-content">
<div class="add-repo-tools">
<button mat-flat-button color="primary" (click)="openAddRepositoryDialog()">Add Repository</button>
<button (click)="openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
</div>
<div class="repository-list">
<div *ngFor="let repository of repositories" class="repository-container">

@ -14,6 +14,7 @@
height: 5em;
display: flex;
flex-direction: row-reverse;
button {
margin: 1em;
}

@ -4,18 +4,28 @@
<p>{{this.getDaemonStatusText()}}</p>
</div>
<mat-card-content>
<p class="repository-path" *ngIf="repository.local">{{repository.path!}}</p>
<p class="repository-address" *ngIf="!repository.local">{{repository.address}}</p>
<p *ngIf="repository.local" class="repository-path">{{repository.path!}}</p>
<p *ngIf="!repository.local" class="repository-address">{{repository.address}}</p>
</mat-card-content>
<mat-action-list>
<button *ngIf="!this.isSelectedRepository() && repository.local" mat-flat-button color="primary" (click)="startDaemonAndSelectRepository()">Open</button>
<button *ngIf="!this.isSelectedRepository() && !repository.local" mat-flat-button color="primary" (click)="selectRepository()" [disabled]="!this.daemonRunning">Connect</button>
<button *ngIf="this.isSelectedRepository() && repository.local" mat-flat-button color="primary" (click)="this.repoService.closeSelectedRepository()">Close</button>
<button *ngIf="this.isSelectedRepository() && !repository.local" mat-flat-button color="primary" (click)="this.repoService.disconnectSelectedRepository()">Disconnect</button>
<button mat-button [mat-menu-trigger-for]="menu" class="menu-button"><mat-icon>more_vert</mat-icon></button>
<button (click)="startDaemonAndSelectRepository()" *ngIf="!this.isSelectedRepository() && repository.local" color="primary"
mat-flat-button>Open
</button>
<button (click)="selectRepository()" *ngIf="!this.isSelectedRepository() && !repository.local" [disabled]="!this.daemonRunning"
color="primary" mat-flat-button>Connect
</button>
<button (click)="this.repoService.closeSelectedRepository()" *ngIf="this.isSelectedRepository() && repository.local" color="primary"
mat-flat-button>Close
</button>
<button (click)="this.repoService.disconnectSelectedRepository()" *ngIf="this.isSelectedRepository() && !repository.local" color="primary"
mat-flat-button>Disconnect
</button>
<button [mat-menu-trigger-for]="menu" class="menu-button" mat-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button *ngIf="repository.local" mat-menu-item (click)="removeRepository()">Delete</button>
<button *ngIf="!repository.local" mat-menu-item (click)="removeRepository()">Remove</button>
<button (click)="removeRepository()" *ngIf="repository.local" mat-menu-item>Delete</button>
<button (click)="removeRepository()" *ngIf="!repository.local" mat-menu-item>Remove</button>
</mat-menu>
</mat-action-list>
</mat-card>

@ -26,9 +26,11 @@
.status-local {
background-color: #2e237a;
}
.status-offline {
background-color: #8e1e2a;
}
.status-online {
background-color: #1b651b;
}

@ -1,5 +1,4 @@
import {Injectable, OnInit} from '@angular/core';
import {BehaviorSubject} from "rxjs";
import {Injectable} from '@angular/core';
import {listen} from "@tauri-apps/api/event";
@Injectable({

@ -1,4 +1,4 @@
import {Inject, Injectable, Sanitizer} from '@angular/core';
import {Inject, Injectable} from '@angular/core';
import {BehaviorSubject} from "rxjs";
import {File} from "../../models/File";
import {invoke} from "@tauri-apps/api/tauri";
@ -24,21 +24,25 @@ export class FileService {
}
public async findFiles(tags: TagQuery[], sortBy: SortKey[]) {
let files = await invoke<File[]>("plugin:mediarepo|find_files", {tags, sortBy: sortBy.map(k => k.toBackendType())});
let files = await invoke<File[]>("plugin:mediarepo|find_files",
{tags, sortBy: sortBy.map(k => k.toBackendType())});
this.displayedFiles.next(files);
}
public async readFile(file: File): Promise<SafeResourceUrl> {
const once_uri = await invoke<string>("plugin:mediarepo|read_file_by_hash", {hash: file.hash, mimeType: file.mime_type});
const once_uri = await invoke<string>("plugin:mediarepo|read_file_by_hash",
{hash: file.hash, mimeType: file.mime_type});
return this.sanitizer.bypassSecurityTrustResourceUrl(once_uri);
}
public async readThumbnail(thumbnail: Thumbnail): Promise<SafeResourceUrl> {
let once_uri = await invoke<string>("plugin:mediarepo|read_thumbnail", {hash: thumbnail.hash, mimeType: thumbnail.mime_type });
let once_uri = await invoke<string>("plugin:mediarepo|read_thumbnail",
{hash: thumbnail.hash, mimeType: thumbnail.mime_type});
return this.sanitizer.bypassSecurityTrustResourceUrl(once_uri);
}
public async getThumbnails(hash: string): Promise<Thumbnail[]> {
return await invoke<Thumbnail[]>("plugin:mediarepo|get_file_thumbnails", {hash});
return await invoke<Thumbnail[]>("plugin:mediarepo|get_file_thumbnails",
{hash});
}
}

@ -1,6 +1,6 @@
import {Injectable} from '@angular/core';
import {Repository} from "../../models/Repository";
import {BehaviorSubject, Observable} from "rxjs";
import {BehaviorSubject} from "rxjs";
import {invoke} from "@tauri-apps/api/tauri";
import {listen} from "@tauri-apps/api/event";
import {Info} from "../../models/Info";
@ -11,7 +11,8 @@ import {ErrorBrokerService} from "../error-broker/error-broker.service";
})
export class RepositoryService {
repositories = new BehaviorSubject<Repository[]>([]);
public selectedRepository = new BehaviorSubject<Repository | undefined>(undefined);
public selectedRepository = new BehaviorSubject<Repository | undefined>(
undefined);
constructor(private errorBroker: ErrorBrokerService) {
this.registerListener()
@ -80,7 +81,8 @@ export class RepositoryService {
* @returns {Promise<void>}
*/
public async addRepository(name: string, path: string | undefined, address: string | undefined, local: boolean) {
let repos = await invoke<Repository[]>("plugin:mediarepo|add_repository", {name, path, address, local});
let repos = await invoke<Repository[]>("plugin:mediarepo|add_repository",
{name, path, address, local});
this.repositories.next(repos);
}
@ -90,7 +92,8 @@ export class RepositoryService {
* @returns {Promise<boolean>}
*/
public async checkDaemonRunning(address: string): Promise<boolean> {
return await invoke<boolean>("plugin:mediarepo|check_daemon_running", {address});
return await invoke<boolean>("plugin:mediarepo|check_daemon_running",
{address});
}
/**
@ -99,7 +102,8 @@ export class RepositoryService {
* @returns {Promise<boolean>}
*/
public async checkLocalRepositoryExists(path: string): Promise<boolean> {
return await invoke<boolean>("plugin:mediarepo|check_local_repository_exists", {path})
return await invoke<boolean>(
"plugin:mediarepo|check_local_repository_exists", {path})
}
/**
@ -126,7 +130,8 @@ export class RepositoryService {
}
async loadSelectedRepository() {
let active_repo = await invoke<Repository | undefined>("plugin:mediarepo|get_active_repository");
let active_repo = await invoke<Repository | undefined>(
"plugin:mediarepo|get_active_repository");
this.selectedRepository.next(active_repo);
}
}

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core';
import {invoke} from "@tauri-apps/api/tauri";
import {Tag} from "../../models/Tag";
import {BehaviorSubject, Observable} from "rxjs";
import {BehaviorSubject} from "rxjs";
@Injectable({
providedIn: 'root'
@ -10,7 +10,8 @@ export class TagService {
public tags: BehaviorSubject<Tag[]> = new BehaviorSubject<Tag[]>([]);
constructor() { }
constructor() {
}
public async loadTags() {
const tags = await invoke<Tag[]>("plugin:mediarepo|get_all_tags");
@ -18,16 +19,19 @@ export class TagService {
}
public async getTagsForFile(hash: string): Promise<Tag[]> {
const tags = await invoke<Tag[]>("plugin:mediarepo|get_tags_for_file", {hash});
const tags = await invoke<Tag[]>("plugin:mediarepo|get_tags_for_file",
{hash});
return tags.map(t => new Tag(t.id, t.name, t.namespace));
}
public async getTagsForFiles(hashes: string[]): Promise<Tag[]> {
let tags: Tag[] = []
if (hashes.length === 1) {
tags = await invoke<Tag[]>("plugin:mediarepo|get_tags_for_file", {hash: hashes[0]});
tags = await invoke<Tag[]>("plugin:mediarepo|get_tags_for_file",
{hash: hashes[0]});
} else if (hashes.length > 0) {
tags = await invoke<Tag[]>("plugin:mediarepo|get_tags_for_files", {hashes});
tags = await invoke<Tag[]>("plugin:mediarepo|get_tags_for_files",
{hashes});
}
return tags.map(t => new Tag(t.id, t.name, t.namespace));
}

@ -4,9 +4,9 @@
<meta charset="utf-8">
<title>MediarepoUi</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com">
<meta content="width=device-width, initial-scale=1" name="viewport">
<link href="favicon.ico" rel="icon" type="image/x-icon">
<link href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>

@ -3,6 +3,7 @@
@use 'src/app/app.component-theme' as app;
@use 'src/app/components/file-grid/file-grid-entry/file-grid-entry.component-theme' as file-grid-entry;
@use 'src/app/components/file-search/file-search.component-theme' as file-search;
@include mat.core();
$theme: mat.define-dark-theme((

Loading…
Cancel
Save