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); const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges(); fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement; 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() { 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); this.errorBroker.infoCb = (info: string) => this.showInfo(info);
await this.repoService.loadRepositories(); await this.repoService.loadRepositories();
} }

@ -4,7 +4,7 @@
<div mat-dialog-content> <div mat-dialog-content>
{{message}} {{message}}
</div> </div>
<div mat-dialog-actions class="confirm-dialog-actions"> <div class="confirm-dialog-actions" mat-dialog-actions>
<button mat-stroked-button [color]="this.denyColor" (click)="closeDialog(false)">{{denyAction}}</button> <button (click)="closeDialog(false)" [color]="this.denyColor" mat-stroked-button>{{denyAction}}</button>
<button mat-flat-button [color]="this.confirmColor" (click)="closeDialog(true)">{{confirmAction}}</button> <button (click)="closeDialog(true)" [color]="this.confirmColor" mat-flat-button>{{confirmAction}}</button>
</div> </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 {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ThemePalette} from "@angular/material/core"; import {ThemePalette} from "@angular/material/core";

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

@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input} from '@angular/core';
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
@Component({ @Component({
@ -14,7 +14,8 @@ export class ContentAwareImageComponent {
scaleWidth = false; scaleWidth = false;
constructor() { } constructor() {
}
/** /**
* Fits the image into the container * 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> <mat-progress-spinner *ngIf="!contentUrl"></mat-progress-spinner>
<app-content-aware-image *ngIf="contentUrl" [imageSrc]="contentUrl"></app-content-aware-image> <app-content-aware-image *ngIf="contentUrl" [imageSrc]="contentUrl"></app-content-aware-image>
</div> </div>

@ -1,10 +1,11 @@
import { import {
Component, Component,
EventEmitter, EventEmitter,
Input, OnChanges, Input,
OnDestroy, OnChanges,
OnInit, OnInit,
Output, SimpleChanges Output,
SimpleChanges
} 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";
@ -25,7 +26,8 @@ export class FileGalleryEntryComponent implements OnInit, OnChanges {
private cachedFile: File | undefined; private cachedFile: File | undefined;
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { } constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) {
}
async ngOnChanges(changes: SimpleChanges): Promise<void> { 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 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 { try {
const hash = this.file.data.hash; const hash = this.file.data.hash;
const thumbnails = await this.fileService.getThumbnails(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]; thumbnail = thumbnail ?? thumbnails[0];
if (!thumbnail) { if (!thumbnail) {

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

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

@ -1,17 +1,21 @@
import { import {
Component, ElementRef, Component,
EventEmitter, HostListener, ElementRef,
EventEmitter,
HostListener,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
Output, SimpleChanges, ViewChild Output,
SimpleChanges,
ViewChild
} 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";
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
import {Selectable} from "../../models/Selectable"; import {Selectable} from "../../models/Selectable";
import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling"; import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
import {CdkDrag, CdkDragMove, DragRef, Point} from "@angular/cdk/drag-drop"; import {CdkDragMove} from "@angular/cdk/drag-drop";
@Component({ @Component({
selector: 'app-file-gallery', selector: 'app-file-gallery',
@ -29,7 +33,8 @@ export class FileGalleryComponent implements OnChanges, OnInit {
@ViewChild("virtualScroll") virtualScroll!: CdkVirtualScrollViewport; @ViewChild("virtualScroll") virtualScroll!: CdkVirtualScrollViewport;
@ViewChild("scaledImage") scaledImage: ElementRef<HTMLDivElement> | undefined; @ViewChild("scaledImage") scaledImage: ElementRef<HTMLDivElement> | undefined;
@ViewChild("imageDragContainer") imageDragContainer: ElementRef<HTMLDivElement> | undefined; @ViewChild(
"imageDragContainer") imageDragContainer: ElementRef<HTMLDivElement> | undefined;
public selectedFile: Selectable<File> | undefined; public selectedFile: Selectable<File> | undefined;
public fileContentUrl: SafeResourceUrl | undefined; public fileContentUrl: SafeResourceUrl | undefined;
@ -58,10 +63,12 @@ export class FileGalleryComponent implements OnChanges, OnInit {
if (this.virtualScroll) { if (this.virtualScroll) {
const viewportSize = this.virtualScroll.getViewportSize(); const viewportSize = this.virtualScroll.getViewportSize();
const indexAdjustment = (viewportSize / 260) / 2; // adjustment to have the selected item centered 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) { 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); this.fileSelectEvent.emit(this.selectedFile.data);
@ -136,6 +143,11 @@ export class FileGalleryComponent implements OnChanges, OnInit {
this.imagePosition = {x: 0, y: 0}; 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"]) @HostListener("window:keydown", ["$event"])
private async handleKeydownEvent(event: KeyboardEvent) { private async handleKeydownEvent(event: KeyboardEvent) {
switch (event.key) { switch (event.key) {
@ -180,9 +192,4 @@ export class FileGalleryComponent implements OnChanges, OnInit {
} }
return undefined; 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-title *ngIf="!!gridEntry.file?.name">{{gridEntry.file?.name}}</mat-card-title>
<mat-card-content *ngIf="contentUrl !== undefined"> <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-content>
<mat-card-footer> <mat-card-footer>
<mat-progress-bar *ngIf="contentUrl === undefined" mode="indeterminate"></mat-progress-bar> <mat-progress-bar *ngIf="contentUrl === undefined" mode="indeterminate"></mat-progress-bar>

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

@ -1,8 +1,10 @@
<div class="file-gallery-inner"> <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 *cdkVirtualFor="let rowEntry of partitionedGridEntries">
<div class="file-row"> <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> *ngFor="let gridEntry of rowEntry" [gridEntry]="gridEntry"></app-file-grid-entry>
</div> </div>
</div> </div>

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

@ -1,25 +1,25 @@
<div class="tag-input-list-and-actions"> <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-list-inner">
<div class="tag-input-item" *ngFor="let tag of searchTags" mat-ripple <div (click)="removeSearchTag(tag)" *ngFor="let tag of searchTags" class="tag-input-item"
(click)="removeSearchTag(tag)">{{tag.getNormalizedTag()}}</div> mat-ripple>{{tag.getNormalizedTag()}}</div>
</div> </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> <mat-icon>delete-sweep</mat-icon>
</button> </button>
</div> </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> <mat-label>Enter tags to filter for</mat-label>
<input matInput <input #tagInput
#tagInput
(keydown)="addSearchTagByInput($event)" (keydown)="addSearchTagByInput($event)"
[formControl]="formControl"
[matAutocomplete]="auto" [matAutocomplete]="auto"
[formControl]="formControl"/> matInput/>
<mat-autocomplete #auto (optionSelected)="addSearchTagByAutocomplete($event)"> <mat-autocomplete #auto (optionSelected)="addSearchTagByAutocomplete($event)">
<mat-option *ngFor="let tag of suggestionTags | async" [value]="tag"> <mat-option *ngFor="let tag of suggestionTags | async" [value]="tag">
{{tag}} {{tag}}
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </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 { import {
AfterViewChecked, AfterViewChecked,
Component, Component,
ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import {TagService} from "../../services/tag/tag.service"; 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; 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() { public async searchForFiles() {
this.searchStartEvent.emit(); this.searchStartEvent.emit();
try { 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> <h1 mat-dialog-title>Sort Entries</h1>
<div mat-dialog-content> <div mat-dialog-content>
<div cdkDropList class="sort-input-list" (cdkDropListDropped)="this.onSortEntryDrop($event)"> <div (cdkDropListDropped)="this.onSortEntryDrop($event)" cdkDropList class="sort-input-list">
<div cdkDrag class="sort-input-row" *ngFor="let sortKey of sortEntries"> <div *ngFor="let sortKey of sortEntries" cdkDrag class="sort-input-row">
<div class="drag-placeholder" *cdkDragPlaceholder></div> <div *cdkDragPlaceholder class="drag-placeholder"></div>
<div class="drag-handle" cdkDragHandle><mat-icon>drag_handle</mat-icon></div> <div cdkDragHandle class="drag-handle">
<mat-icon>drag_handle</mat-icon>
</div>
<mat-form-field> <mat-form-field>
<mat-label>Key</mat-label> <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="Namespace">Namespace</mat-option>
<mat-option value="FileName">File Name</mat-option> <mat-option value="FileName">File Name</mat-option>
<mat-option value="FileSize">File Size</mat-option> <mat-option value="FileSize">File Size</mat-option>
@ -19,9 +21,10 @@
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="sortKey.sortType === 'Namespace'"> <mat-form-field *ngIf="sortKey.sortType === 'Namespace'">
<mat-label>Namespace Name</mat-label> <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> </mat-form-field>
<div class="filler" *ngIf="sortKey.sortType !== 'Namespace'"></div> <div *ngIf="sortKey.sortType !== 'Namespace'" class="filler"></div>
<mat-form-field> <mat-form-field>
<mat-label>Direction</mat-label> <mat-label>Direction</mat-label>
<mat-select [(value)]="sortKey.sortDirection" required> <mat-select [(value)]="sortKey.sortDirection" required>
@ -29,12 +32,17 @@
<mat-option value="Descending">Descending</mat-option> <mat-option value="Descending">Descending</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button *ngIf="sortEntries.indexOf(sortKey) === sortEntries.length -1" mat-flat-button (click)="addNewSortKey()"><mat-icon>add</mat-icon></button> <button (click)="addNewSortKey()" *ngIf="sortEntries.indexOf(sortKey) === sortEntries.length -1" mat-flat-button>
<button *ngIf="sortEntries.indexOf(sortKey) !== sortEntries.length -1" mat-flat-button (click)="removeSortKey(sortKey)"><mat-icon>remove</mat-icon></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>
</div> </div>
<div class="dialog-actions" mat-dialog-actions> <div class="dialog-actions" mat-dialog-actions>
<button mat-flat-button color="primary" (click)="confirmSort()">Sort</button> <button (click)="confirmSort()" color="primary" mat-flat-button>Sort</button>
<button mat-stroked-button color="accent" (click)="cancelSort()">Cancel</button> <button (click)="cancelSort()" color="accent" mat-stroked-button>Cancel</button>
</div> </div>

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

@ -12,7 +12,8 @@ export class FilterDialogComponent {
public sortEntries: SortKey[] = [] 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; this.sortEntries = data.sortEntries;
} }

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

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

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

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

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

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

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

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

@ -1,19 +1,20 @@
<mat-drawer-container class="page"> <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" <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>
<mat-drawer-content> <mat-drawer-content>
<div *ngIf="contentLoading" class="spinner-overlay"> <div *ngIf="contentLoading" class="spinner-overlay">
<mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner> <mat-progress-spinner color="primary" mode="indeterminate"></mat-progress-spinner>
</div> </div>
<app-file-grid *ngIf="!this.showGallery" (fileDblClickEvent)="openGallery($event)" [files]="files" <app-file-grid (fileDblClickEvent)="openGallery($event)" (fileMultiselectEvent)="onFileMultiSelect($event)" (fileSelectEvent)="onFileSelect($event)"
(fileSelectEvent)="onFileSelect($event)" *ngIf="!this.showGallery"
[files]="files"
[preselectedFile]="this.preselectedFile" [preselectedFile]="this.preselectedFile"
(fileMultiselectEvent)="onFileMultiSelect($event)"
></app-file-grid> ></app-file-grid>
<app-file-gallery *ngIf="this.showGallery" [files]="files" (fileSelectEvent)="onFileSelect($event)" <app-file-gallery (closeEvent)="this.closeGallery($event.selectedFile?.data)" (fileSelectEvent)="onFileSelect($event)" *ngIf="this.showGallery"
[preselectedFile]="this.preselectedFile" [files]="files"
(closeEvent)="this.closeGallery($event.selectedFile?.data)"></app-file-gallery> [preselectedFile]="this.preselectedFile"></app-file-gallery>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>

@ -1,13 +1,7 @@
import {Component, OnInit, ViewChild} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Tag} from "../../../models/Tag";
import {File} from "../../../models/File"; import {File} from "../../../models/File";
import {FileSearchComponent} from "../../../components/file-search/file-search.component";
import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service"; import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service";
import {FileService} from "../../../services/file/file.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"; import {RepositoryService} from "../../../services/repository/repository.service";
@Component({ @Component({

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

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

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

@ -1,6 +1,6 @@
<div class="repo-page-content"> <div class="repo-page-content">
<div class="add-repo-tools"> <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>
<div class="repository-list"> <div class="repository-list">
<div *ngFor="let repository of repositories" class="repository-container"> <div *ngFor="let repository of repositories" class="repository-container">

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

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

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

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

@ -1,4 +1,4 @@
import {Inject, Injectable, Sanitizer} from '@angular/core'; import {Inject, Injectable} from '@angular/core';
import {BehaviorSubject} from "rxjs"; import {BehaviorSubject} from "rxjs";
import {File} from "../../models/File"; import {File} from "../../models/File";
import {invoke} from "@tauri-apps/api/tauri"; import {invoke} from "@tauri-apps/api/tauri";
@ -24,21 +24,25 @@ export class FileService {
} }
public async findFiles(tags: TagQuery[], sortBy: SortKey[]) { 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); this.displayedFiles.next(files);
} }
public async readFile(file: File): Promise<SafeResourceUrl> { 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); return this.sanitizer.bypassSecurityTrustResourceUrl(once_uri);
} }
public async readThumbnail(thumbnail: Thumbnail): Promise<SafeResourceUrl> { 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); return this.sanitizer.bypassSecurityTrustResourceUrl(once_uri);
} }
public async getThumbnails(hash: string): Promise<Thumbnail[]> { 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 {Injectable} from '@angular/core';
import {Repository} from "../../models/Repository"; import {Repository} from "../../models/Repository";
import {BehaviorSubject, Observable} from "rxjs"; import {BehaviorSubject} from "rxjs";
import {invoke} from "@tauri-apps/api/tauri"; import {invoke} from "@tauri-apps/api/tauri";
import {listen} from "@tauri-apps/api/event"; import {listen} from "@tauri-apps/api/event";
import {Info} from "../../models/Info"; import {Info} from "../../models/Info";
@ -11,7 +11,8 @@ import {ErrorBrokerService} from "../error-broker/error-broker.service";
}) })
export class RepositoryService { export class RepositoryService {
repositories = new BehaviorSubject<Repository[]>([]); repositories = new BehaviorSubject<Repository[]>([]);
public selectedRepository = new BehaviorSubject<Repository | undefined>(undefined); public selectedRepository = new BehaviorSubject<Repository | undefined>(
undefined);
constructor(private errorBroker: ErrorBrokerService) { constructor(private errorBroker: ErrorBrokerService) {
this.registerListener() this.registerListener()
@ -80,7 +81,8 @@ export class RepositoryService {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async addRepository(name: string, path: string | undefined, address: string | undefined, local: boolean) { 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); this.repositories.next(repos);
} }
@ -90,7 +92,8 @@ export class RepositoryService {
* @returns {Promise<boolean>} * @returns {Promise<boolean>}
*/ */
public async checkDaemonRunning(address: string): 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>} * @returns {Promise<boolean>}
*/ */
public async checkLocalRepositoryExists(path: string): 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() { 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); this.selectedRepository.next(active_repo);
} }
} }

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

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

@ -3,6 +3,7 @@
@use 'src/app/app.component-theme' as app; @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-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; @use 'src/app/components/file-search/file-search.component-theme' as file-search;
@include mat.core(); @include mat.core();
$theme: mat.define-dark-theme(( $theme: mat.define-dark-theme((

Loading…
Cancel
Save