Cleanup code

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent 9773444d7f
commit 60faf81187

@ -1,11 +1,7 @@
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NgIconsModule} from "@ng-icons/core";
import * as materialIcons from "@ng-icons/material-icons";
import {CoreModule} from "./components/core/core.module"; import {CoreModule} from "./components/core/core.module";
@NgModule({ @NgModule({

@ -20,8 +20,8 @@ export class CoreComponent implements OnInit {
constructor( constructor(
private tabService: TabService, private tabService: TabService,
private repoService: RepositoryService, private repoService: RepositoryService,
private tagService: TagService) 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();

@ -7,34 +7,14 @@ import {FilesTabSidebarComponent} from "./files-tab/files-tab-sidebar/files-tab-
import {ImportTabComponent} from "./import-tab/import-tab.component"; import {ImportTabComponent} from "./import-tab/import-tab.component";
import {ImportTabSidebarComponent} from "./import-tab/import-tab-sidebar/import-tab-sidebar.component"; import {ImportTabSidebarComponent} from "./import-tab/import-tab-sidebar/import-tab-sidebar.component";
import {FilesystemImportComponent} from "./import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component"; import {FilesystemImportComponent} from "./import-tab/import-tab-sidebar/filesystem-import/filesystem-import.component";
import {MatCardModule} from "@angular/material/card";
import {MatListModule} from "@angular/material/list";
import {MatButtonModule} from "@angular/material/button"; import {MatButtonModule} from "@angular/material/button";
import {MatToolbarModule} from "@angular/material/toolbar";
import {MatSnackBarModule} from "@angular/material/snack-bar";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {ReactiveFormsModule} from "@angular/forms";
import {MatSidenavModule} from "@angular/material/sidenav"; import {MatSidenavModule} from "@angular/material/sidenav";
import {MatGridListModule} from "@angular/material/grid-list";
import {MatProgressBarModule} from "@angular/material/progress-bar"; import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatPaginatorModule} from "@angular/material/paginator";
import {ScrollingModule} from "@angular/cdk/scrolling"; import {ScrollingModule} from "@angular/cdk/scrolling";
import {MatChipsModule} from "@angular/material/chips";
import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {MatTabsModule} from "@angular/material/tabs"; import {MatTabsModule} from "@angular/material/tabs";
import {FlexModule, GridModule} from "@angular/flex-layout"; import {FlexModule} from "@angular/flex-layout";
import {MatOptionModule, MatRippleModule} from "@angular/material/core"; import {MatOptionModule} from "@angular/material/core";
import {MatDialogModule} from "@angular/material/dialog";
import {MatSelectModule} from "@angular/material/select"; import {MatSelectModule} from "@angular/material/select";
import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {BlockUIModule} from "primeng/blockui";
import {PanelModule} from "primeng/panel";
import {DragDropModule} from "@angular/cdk/drag-drop";
import {MatSliderModule} from "@angular/material/slider";
import {MatTooltipModule} from "@angular/material/tooltip";
import {MatMenuModule} from "@angular/material/menu";
import {MatExpansionModule} from "@angular/material/expansion";
import {MatCheckboxModule} from "@angular/material/checkbox"; import {MatCheckboxModule} from "@angular/material/checkbox";
import {SharedModule} from "../shared/shared.module"; import {SharedModule} from "../shared/shared.module";
import {MatDividerModule} from "@angular/material/divider"; import {MatDividerModule} from "@angular/material/divider";

@ -14,23 +14,28 @@
</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 (click)="addSearchTag(tag)" *cdkVirtualFor="let tag of tags" class="selectable-tag" matRipple <div (click)="addSearchTag(tag)" (contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)" *cdkVirtualFor="let tag of tags" class="selectable-tag"
(contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)"> 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>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="Edit" *ngIf="this.selectedFiles.length > 0"> <mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit">
<app-file-edit #fileedit [files]="this.selectedFiles"></app-file-edit> <app-file-edit #fileedit [files]="this.selectedFiles"></app-file-edit>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
<app-context-menu #contextMenu> <app-context-menu #contextMenu>
<button *ngIf="this.contextMenuTag" mat-menu-item (click)="this.copyToClipboard(this.contextMenuTag!.getNormalizedOutput())">Copy "{{contextMenuTag!.getNormalizedOutput()}}"</button> <button (click)="this.copyToClipboard(this.contextMenuTag!.getNormalizedOutput())" *ngIf="this.contextMenuTag"
<button *ngIf="this.contextMenuTag?.namespace" mat-menu-item (click)="this.copyToClipboard(this.contextMenuTag!.name)">Copy "{{this.contextMenuTag!.name}}"</button> mat-menu-item>Copy
<button *ngIf="this.contextMenuTag?.namespace" mat-menu-item "{{contextMenuTag!.getNormalizedOutput()}}"
(click)="this.copyToClipboard(this.contextMenuTag!.namespace!)">Copy "{{this.contextMenuTag!.namespace!}}" </button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.name)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.name}}"
</button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.namespace!)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.namespace!}}"
</button> </button>
</app-context-menu> </app-context-menu>

@ -49,6 +49,7 @@ cdk-virtual-scroll-viewport {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: auto; overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper { ::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%; width: 100%;
} }

@ -75,12 +75,17 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
} }
async showFileDetails(files: File[]) { async showFileDetails(files: File[]) {
this.tagsOfSelection = await this.tagService.getTagsForFiles(files.map(f => f.hash)) this.tagsOfSelection = await this.tagService.getTagsForFiles(
files.map(f => f.hash))
this.tagsOfSelection = this.tagsOfSelection.sort( this.tagsOfSelection = this.tagsOfSelection.sort(
(a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput())); (a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput()));
this.tags = this.tagsOfSelection; this.tags = this.tagsOfSelection;
} }
public async copyToClipboard(text: string) {
await clipboard.writeText(text);
}
private async refreshFileSelection() { private async refreshFileSelection() {
const filteredSelection = this.selectedFiles.filter( const filteredSelection = this.selectedFiles.filter(
file => this.files.findIndex(f => f.id === file.id) >= 0); file => this.files.findIndex(f => f.id === file.id) >= 0);
@ -100,8 +105,4 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
.localeCompare(b.getNormalizedOutput())); .localeCompare(b.getNormalizedOutput()));
} }
} }
public async copyToClipboard(text: string) {
await clipboard.writeText(text);
}
} }

@ -5,7 +5,7 @@
[selectedFiles]="this.selectedFiles"></app-files-tab-sidebar> [selectedFiles]="this.selectedFiles"></app-files-tab-sidebar>
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<app-busy-indicator [busy]="contentLoading" [blurBackground]="true"> <app-busy-indicator [blurBackground]="true" [busy]="contentLoading">
<app-file-multiview (fileSelectEvent)="this.onFileSelect($event)" [files]="this.files"></app-file-multiview> <app-file-multiview (fileSelectEvent)="this.onFileSelect($event)" [files]="this.files"></app-file-multiview>
</app-busy-indicator> </app-busy-indicator>
</mat-drawer-content> </mat-drawer-content>

@ -5,26 +5,31 @@
<mat-option value="files">Files</mat-option> <mat-option value="files">Files</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<app-native-file-select [filters]="this.filters" [mode]="selectionType.value" (fileSelect)="this.setSelectedPaths($event)"></app-native-file-select> <app-native-file-select (fileSelect)="this.setSelectedPaths($event)" [filters]="this.filters"
[mode]="selectionType.value"></app-native-file-select>
<button mat-flat-button> <button mat-flat-button>
{{resolving ? "Searching for files..." : this.fileCount + " files found"}} {{resolving ? "Searching for files..." : this.fileCount + " files found"}}
<mat-progress-bar *ngIf="resolving" mode="indeterminate" color="primary"></mat-progress-bar> <mat-progress-bar *ngIf="resolving" color="primary" mode="indeterminate"></mat-progress-bar>
</button> </button>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<section class="binary-import-options"> <section class="binary-import-options">
<mat-checkbox [checked]="this.importOptions.read_tags_from_txt" (change)="this.importOptions.read_tags_from_txt = $event.checked">Import tags from <mat-checkbox (change)="this.importOptions.read_tags_from_txt = $event.checked"
[checked]="this.importOptions.read_tags_from_txt">Import tags from
adjacent .txt tag files adjacent .txt tag files
</mat-checkbox> </mat-checkbox>
<mat-checkbox [checked]="this.importOptions.delete_after_import" (change)="this.importOptions.delete_after_import = $event.checked" color="warn"> <mat-checkbox (change)="this.importOptions.delete_after_import = $event.checked"
[checked]="this.importOptions.delete_after_import" color="warn">
Delete files from original location after import Delete files from original location after import
</mat-checkbox> </mat-checkbox>
</section> </section>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<button mat-flat-button color="primary" class="import-button" [disabled]="importing || this.fileCount === 0" (click)="import()"> <button (click)="import()" [disabled]="importing || this.fileCount === 0" class="import-button" color="primary"
mat-flat-button>
{{importing ? "Importing..." : "Import"}} {{importing ? "Importing..." : "Import"}}
</button> </button>
<mat-progress-bar *ngIf="importing" mode="determinate" color="primary" [value]="this.importingProgress"></mat-progress-bar> <mat-progress-bar *ngIf="importing" [value]="this.importingProgress" color="primary"
mode="determinate"></mat-progress-bar>

@ -12,7 +12,8 @@
<mat-divider></mat-divider> <mat-divider></mat-divider>
</div> </div>
<div class="import-configuration" fxFlex fxFlexFill> <div class="import-configuration" fxFlex fxFlexFill>
<app-filesystem-import (fileImported)="this.fileImported.emit($event)" (importFinished)="importFinished.emit()"></app-filesystem-import> <app-filesystem-import (fileImported)="this.fileImported.emit($event)"
(importFinished)="importFinished.emit()"></app-filesystem-import>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>

@ -18,6 +18,7 @@ mat-tab-group, mat-tab {
width: calc(100% - 2em); width: calc(100% - 2em);
height: calc(100% - 2em); height: calc(100% - 2em);
margin: 1em; margin: 1em;
mat-select { mat-select {
height: 100%; height: 100%;
} }

@ -1,11 +1,4 @@
import { import {Component, EventEmitter, Output} from '@angular/core';
Component,
EventEmitter,
OnInit,
Output,
ViewChild
} from '@angular/core';
import {MatTabGroup} from "@angular/material/tabs";
import {File} from "../../../../models/File"; import {File} from "../../../../models/File";
@Component({ @Component({
@ -18,5 +11,6 @@ export class ImportTabSidebarComponent {
@Output() fileImported = new EventEmitter<File>(); @Output() fileImported = new EventEmitter<File>();
@Output() importFinished = new EventEmitter<void>(); @Output() importFinished = new EventEmitter<void>();
constructor() { } constructor() {
}
} }

@ -1,6 +1,7 @@
<mat-drawer-container autosize> <mat-drawer-container autosize>
<mat-drawer disableClose="true" mode="side" opened> <mat-drawer disableClose="true" mode="side" opened>
<app-import-tab-sidebar (fileImported)="this.addFileFromImport($event)" (importFinished)="this.refreshFileView()"></app-import-tab-sidebar> <app-import-tab-sidebar (fileImported)="this.addFileFromImport($event)"
(importFinished)="this.refreshFileView()"></app-import-tab-sidebar>
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<app-file-multiview [files]="this.files"></app-file-multiview> <app-file-multiview [files]="this.files"></app-file-multiview>

@ -1,4 +1,5 @@
<ng-content></ng-content> <ng-content></ng-content>
<div *ngIf="this.busy" class="busy-indicator-overlay" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground"> <div *ngIf="this.busy" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground"
<mat-progress-spinner color="primary" [mode]="mode" [value]="value"></mat-progress-spinner> class="busy-indicator-overlay">
<mat-progress-spinner [mode]="mode" [value]="value" color="primary"></mat-progress-spinner>
</div> </div>

@ -1,4 +1,4 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input} from '@angular/core';
import {ProgressSpinnerMode} from "@angular/material/progress-spinner"; import {ProgressSpinnerMode} from "@angular/material/progress-spinner";
@Component({ @Component({
@ -14,7 +14,8 @@ export class BusyIndicatorComponent {
@Input() mode: ProgressSpinnerMode = "indeterminate"; @Input() mode: ProgressSpinnerMode = "indeterminate";
@Input() value: number | undefined; @Input() value: number | undefined;
constructor() { } constructor() {
}
public setBusy(busy: boolean) { public setBusy(busy: boolean) {
this.busy = busy; this.busy = busy;

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

@ -1,4 +1,4 @@
<div class="menu-anchor" [matMenuTriggerFor]="contextMenu" [style.left]="x" [style.top]="y"></div> <div [matMenuTriggerFor]="contextMenu" [style.left]="x" [style.top]="y" class="menu-anchor"></div>
<mat-menu #contextMenu="matMenu"> <mat-menu #contextMenu="matMenu">
<ng-content></ng-content> <ng-content></ng-content>
</mat-menu> </mat-menu>

@ -1,7 +1,4 @@
import { import {Component, ViewChild,} from '@angular/core';
Component,
ViewChild,
} from '@angular/core';
import {MatMenuTrigger} from "@angular/material/menu"; import {MatMenuTrigger} from "@angular/material/menu";
@Component({ @Component({

@ -1,5 +1,5 @@
<app-context-menu #contextMenu> <app-context-menu #contextMenu>
<button mat-menu-item (click)="this.copyFileHash()">Copy Hash</button> <button (click)="this.copyFileHash()" mat-menu-item>Copy Hash</button>
<button mat-menu-item (click)="this.exportFile()">Save As...</button> <button (click)="this.exportFile()" mat-menu-item>Save As...</button>
<ng-content></ng-content> <ng-content></ng-content>
</app-context-menu> </app-context-menu>

@ -1,10 +1,9 @@
import {Component, ViewChild} from '@angular/core'; import {Component, ViewChild} from '@angular/core';
import {File} from "../../../../models/File"; import {File} from "../../../../models/File";
import {ContextMenuComponent} from "../context-menu.component"; import {ContextMenuComponent} from "../context-menu.component";
import {clipboard, dialog} from "@tauri-apps/api"; import {clipboard} from "@tauri-apps/api";
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 {downloadDir} from "@tauri-apps/api/path";
import {FileHelper} from "../../../../services/file/file.helper"; import {FileHelper} from "../../../../services/file/file.helper";
@Component({ @Component({
@ -18,7 +17,8 @@ export class FileContextMenuComponent {
@ViewChild("contextMenu") contextMenu!: ContextMenuComponent; @ViewChild("contextMenu") contextMenu!: ContextMenuComponent;
constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) { } constructor(private fileService: FileService, private errorBroker: ErrorBrokerService) {
}
public onContextMenu(event: MouseEvent, file: File) { public onContextMenu(event: MouseEvent, file: File) {
this.file = file; this.file = file;

@ -3,14 +3,14 @@
<h1>Edit File</h1> <h1>Edit File</h1>
<mat-form-field *ngIf="this.files.length === 1" appearance="fill"> <mat-form-field *ngIf="this.files.length === 1" appearance="fill">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input #fileNameInput matInput (focusout)="this.changeFileName(fileNameInput.value)"> <input #fileNameInput (focusout)="this.changeFileName(fileNameInput.value)" matInput>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tag-edit-list" fxFlex fxFlexFill fxFlexAlign="start"> <div class="tag-edit-list" fxFlex fxFlexAlign="start" fxFlexFill>
<cdk-virtual-scroll-viewport #tagScroll itemSize="50" maxBufferPx="2000" minBufferPx="1000"> <cdk-virtual-scroll-viewport #tagScroll itemSize="50" maxBufferPx="2000" minBufferPx="1000">
<div class="editable-tag" *cdkVirtualFor="let tag of tags"> <div *cdkVirtualFor="let tag of tags" class="editable-tag">
<app-tag-item [tag]="tag"></app-tag-item> <app-tag-item [tag]="tag"></app-tag-item>
<button class="tag-remove-button" mat-icon-button (click)="removeTag(tag)"> <button (click)="removeTag(tag)" class="tag-remove-button" mat-icon-button>
<ng-icon name="mat-remove"></ng-icon> <ng-icon name="mat-remove"></ng-icon>
</button> </button>
</div> </div>
@ -19,14 +19,15 @@
<mat-divider fxFlex="1em"></mat-divider> <mat-divider fxFlex="1em"></mat-divider>
<div class="tag-input" fxFlex="200px"> <div class="tag-input" fxFlex="200px">
<div class="tag-input-field"> <div class="tag-input-field">
<app-tag-input [availableTags]="this.allTags" [allowInvalid]="true" (tagAdded)="this.editTag($event)"></app-tag-input> <app-tag-input (tagAdded)="this.editTag($event)" [allowInvalid]="true"
<button mat-icon-button class="add-tag-button"> [availableTags]="this.allTags"></app-tag-input>
<button class="add-tag-button" mat-icon-button>
<ng-icon *ngIf="editMode === 'Toggle'" name="mat-change-circle"></ng-icon> <ng-icon *ngIf="editMode === 'Toggle'" name="mat-change-circle"></ng-icon>
<ng-icon *ngIf="editMode === 'Add'" name="mat-add-circle"></ng-icon> <ng-icon *ngIf="editMode === 'Add'" name="mat-add-circle"></ng-icon>
<ng-icon *ngIf="editMode === 'Remove'" name="mat-remove-circle"></ng-icon> <ng-icon *ngIf="editMode === 'Remove'" name="mat-remove-circle"></ng-icon>
</button> </button>
</div> </div>
<mat-form-field class="form-field-mode" appearance="fill"> <mat-form-field appearance="fill" class="form-field-mode">
<mat-label>Mode</mat-label> <mat-label>Mode</mat-label>
<mat-select #modeSelect [(value)]="editMode"> <mat-select #modeSelect [(value)]="editMode">
<mat-option value="Toggle">Toggle</mat-option> <mat-option value="Toggle">Toggle</mat-option>

@ -27,6 +27,7 @@ cdk-virtual-scroll-viewport {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: auto; overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper { ::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%; width: 100%;
} }
@ -56,6 +57,7 @@ cdk-virtual-scroll-viewport {
.tag-input-field { .tag-input-field {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
.add-tag-button { .add-tag-button {
width: 65px; width: 65px;
height: 65px; height: 65px;

@ -1,5 +1,6 @@
import { import {
Component, ElementRef, Component,
ElementRef,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
@ -23,12 +24,10 @@ export class FileEditComponent implements OnInit, OnChanges {
public tags: Tag[] = []; public tags: Tag[] = [];
public allTags: Tag[] = []; public allTags: Tag[] = [];
private fileTags: {[key: number]: Tag[]} = {};
public editMode: string = "Toggle"; public editMode: string = "Toggle";
@ViewChild("tagScroll") tagScroll!: CdkVirtualScrollViewport; @ViewChild("tagScroll") tagScroll!: CdkVirtualScrollViewport;
@ViewChild("fileNameInput") fileNameInput!: ElementRef<HTMLInputElement>; @ViewChild("fileNameInput") fileNameInput!: ElementRef<HTMLInputElement>;
private fileTags: { [key: number]: Tag[] } = {};
constructor( constructor(
private tagService: TagService, private tagService: TagService,
@ -95,7 +94,8 @@ export class FileEditComponent implements OnInit, OnChanges {
} else { } else {
removedTags.push(tag.id); removedTags.push(tag.id);
} }
this.fileTags[file.id] = await this.tagService.changeFileTags(file.id, addedTags, removedTags); this.fileTags[file.id] = await this.tagService.changeFileTags(file.id,
addedTags, removedTags);
} }
this.mapFileTagsToTagList(); this.mapFileTagsToTagList();
const index = this.tags.indexOf(tag); const index = this.tags.indexOf(tag);
@ -126,7 +126,8 @@ export class FileEditComponent implements OnInit, OnChanges {
private async loadFileTags() { private async loadFileTags() {
for (const file of this.files) { for (const file of this.files) {
this.fileTags[file.id] = await this.tagService.getTagsForFiles([file.hash]); this.fileTags[file.id] = await this.tagService.getTagsForFiles(
[file.hash]);
} }
this.mapFileTagsToTagList(); this.mapFileTagsToTagList();
} }
@ -141,8 +142,10 @@ export class FileEditComponent implements OnInit, OnChanges {
let tags: Tag[] = []; let tags: Tag[] = [];
for (const file of this.files) { for (const file of this.files) {
const fileTags = this.fileTags[file.id]; const fileTags = this.fileTags[file.id];
tags.push(...fileTags.filter(t => tags.findIndex(tag => tag.id === t.id) < 0)); tags.push(
...fileTags.filter(t => tags.findIndex(tag => tag.id === t.id) < 0));
} }
this.tags = tags.sort((a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput())); this.tags = tags.sort(
(a, b) => a.getNormalizedOutput().localeCompare(b.getNormalizedOutput()));
} }
} }

@ -1,4 +1,4 @@
<div class="audio-container"> <div class="audio-container">
<audio controls [src]="this.blobUrl"> <audio [src]="this.blobUrl" controls>
</audio> </audio>
</div> </div>

@ -2,6 +2,7 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
audio { audio {
margin: auto; margin: auto;
} }

@ -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({
@ -10,5 +10,6 @@ export class AudioViewerComponent {
@Input() blobUrl!: SafeResourceUrl; @Input() blobUrl!: SafeResourceUrl;
constructor() { } constructor() {
}
} }

@ -4,6 +4,6 @@
<div *ngIf="getContentType() === 'other'" class="download-prompt"> <div *ngIf="getContentType() === 'other'" class="download-prompt">
<span>Unsupported content type <b>{{this.file.mime_type}}</b></span> <span>Unsupported content type <b>{{this.file.mime_type}}</b></span>
<button mat-flat-button color="primary" (click)="this.downloadContent()">Download</button> <button (click)="this.downloadContent()" color="primary" mat-flat-button>Download</button>
</div> </div>
<app-busy-indicator></app-busy-indicator> <app-busy-indicator></app-busy-indicator>

@ -8,10 +8,12 @@ app-image-viewer, app-video-viewer, app-audio-viewer {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
button { button {
margin: 1em 0 auto; margin: 1em 0 auto;
align-self: center; align-self: center;
} }
span { span {
margin: auto 0 0; margin: auto 0 0;
align-self: center; align-self: center;

@ -1,11 +1,11 @@
import { import {
AfterContentInit, AfterViewInit, AfterViewInit,
Component, Component,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, SimpleChanges,
SimpleChanges, ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
import {File} from "../../../../../models/File"; import {File} from "../../../../../models/File";
@ -45,7 +45,8 @@ export class ContentViewerComponent implements AfterViewInit, OnChanges, OnDestr
public async ngOnChanges(changes: SimpleChanges) { public async ngOnChanges(changes: SimpleChanges) {
if (changes["file"]) { if (changes["file"]) {
if (["audio", "video"].includes(this.getContentType()) && this.busyIndicator) { if (["audio", "video"].includes(
this.getContentType()) && this.busyIndicator) {
await this.loadBlobUrl(); await this.loadBlobUrl();
} else { } else {
this.contentUrl = this.fileService.buildContentUrl(this.file); this.contentUrl = this.fileService.buildContentUrl(this.file);

@ -1,10 +1,9 @@
import { import {
Component, Component,
ElementRef,
HostListener, HostListener,
Input, OnChanges, Input,
OnInit, SimpleChanges, OnChanges,
ViewChild SimpleChanges
} from '@angular/core'; } from '@angular/core';
import {CdkDragMove} from "@angular/cdk/drag-drop"; import {CdkDragMove} from "@angular/cdk/drag-drop";
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
@ -20,7 +19,8 @@ export class ImageViewerComponent implements OnChanges {
public imagePosition = {x: 0, y: 0}; public imagePosition = {x: 0, y: 0};
public mouseInImageView = false; public mouseInImageView = false;
constructor() { } constructor() {
}
public ngOnChanges(changes: SimpleChanges): void { public ngOnChanges(changes: SimpleChanges): void {
if (changes["imageUrl"]) { if (changes["imageUrl"]) {

@ -1,3 +1,3 @@
<video controls [src]="this.blobUrl"> <video [src]="this.blobUrl" controls>
Unsupported video type Unsupported video type
</video> </video>

@ -1,7 +1,4 @@
import { import {Component, Input,} from '@angular/core';
Component,
Input,
} from '@angular/core';
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
@Component({ @Component({

@ -1,6 +1,7 @@
import { import {
Component, Component,
EventEmitter, Inject, EventEmitter,
Inject,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
@ -27,7 +28,8 @@ export class FileGalleryEntryComponent implements OnInit, OnChanges {
private cachedFile: File | undefined; private cachedFile: File | undefined;
private urlSetTimeout: number | undefined; private urlSetTimeout: number | undefined;
constructor(@Inject(DomSanitizer) private sanitizer: DomSanitizer, private fileService: FileService, private errorBroker: ErrorBrokerService) { constructor(@Inject(
DomSanitizer) private sanitizer: DomSanitizer, private fileService: FileService, private errorBroker: ErrorBrokerService) {
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
@ -45,6 +47,8 @@ export class FileGalleryEntryComponent implements OnInit, OnChanges {
private setImageDelayed() { private setImageDelayed() {
this.contentUrl = undefined; this.contentUrl = undefined;
clearTimeout(this.urlSetTimeout); clearTimeout(this.urlSetTimeout);
this.urlSetTimeout = setTimeout(() => this.contentUrl = this.fileService.buildThumbnailUrl(this.file.data, 250, 250), 200); this.urlSetTimeout = setTimeout(
() => this.contentUrl = this.fileService.buildThumbnailUrl(this.file.data,
250, 250), 200);
} }
} }

@ -5,7 +5,8 @@
<div (dblclick)="this.selectedFile? this.fileDblClickEvent.emit(this.selectedFile.data) : null" class="file-full-view" <div (dblclick)="this.selectedFile? this.fileDblClickEvent.emit(this.selectedFile.data) : null" class="file-full-view"
fxFlex="80%"> fxFlex="80%">
<app-content-viewer <app-content-viewer
(contextmenu)="this.selectedFile && fileContextMenu.onContextMenu($event, this.selectedFile!.data)" [file]="this.selectedFile!.data"></app-content-viewer> (contextmenu)="this.selectedFile && fileContextMenu.onContextMenu($event, this.selectedFile!.data)"
[file]="this.selectedFile!.data"></app-content-viewer>
</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%">

@ -62,28 +62,14 @@ export class FileGalleryComponent implements OnChanges, OnInit {
} }
} }
private scrollToSelection(): void {
if (this.selectedFile) {
const selectedIndex = this.entries.indexOf(this.selectedFile);
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");
if (selectedIndex > indexAdjustment) {
this.virtualScroll.scrollToOffset(
this.virtualScroll.measureScrollOffset("left") + 130, "smooth");
}
}
}
/** /**
* Loads the content url of the selected file * Loads the content url of the selected file
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadSelectedFile() { async loadSelectedFile() {
if (this.selectedFile) { if (this.selectedFile) {
this.fileContentUrl = this.fileService.buildContentUrl(this.selectedFile.data) this.fileContentUrl = this.fileService.buildContentUrl(
this.selectedFile.data)
} }
} }
@ -140,6 +126,27 @@ export class FileGalleryComponent implements OnChanges, OnInit {
} }
} }
public adjustElementSizes(): void {
if (this.virtualScroll) {
this.virtualScroll.checkViewportSize();
this.scrollToSelection();
}
}
private scrollToSelection(): void {
if (this.selectedFile) {
const selectedIndex = this.entries.indexOf(this.selectedFile);
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");
if (selectedIndex > indexAdjustment) {
this.virtualScroll.scrollToOffset(
this.virtualScroll.measureScrollOffset("left") + 130, "smooth");
}
}
}
@HostListener("window:keydown", ["$event"]) @HostListener("window:keydown", ["$event"])
private async handleKeydownEvent(event: KeyboardEvent) { private async handleKeydownEvent(event: KeyboardEvent) {
@ -163,11 +170,4 @@ export class FileGalleryComponent implements OnChanges, OnInit {
} }
return undefined; return undefined;
} }
public adjustElementSizes(): void {
if (this.virtualScroll) {
this.virtualScroll.checkViewportSize();
this.scrollToSelection();
}
}
} }

@ -49,7 +49,9 @@ export class FileGridEntryComponent implements OnInit, OnChanges {
private setImageDelayed() { private setImageDelayed() {
this.contentUrl = undefined; this.contentUrl = undefined;
clearTimeout(this.urlSetTimeout); clearTimeout(this.urlSetTimeout);
this.urlSetTimeout = setTimeout(() => this.contentUrl = this.fileService.buildThumbnailUrl(this.gridEntry.file, this.urlSetTimeout = setTimeout(
() => this.contentUrl = this.fileService.buildThumbnailUrl(
this.gridEntry.file,
250, 250), 200); 250, 250), 200);
} }
} }

@ -4,9 +4,9 @@
<div *cdkVirtualFor="let rowEntry of partitionedGridEntries"> <div *cdkVirtualFor="let rowEntry of partitionedGridEntries">
<div class="file-row"> <div class="file-row">
<app-file-grid-entry <app-file-grid-entry
*ngFor="let gridEntry of rowEntry" (clickEvent)="setSelectedFile($event.gridEntry)" (clickEvent)="setSelectedFile($event.gridEntry)" (contextmenu)="fileContextMenu.onContextMenu($event, gridEntry.file)"
(dblClickEvent)="fileOpenEvent.emit($event.gridEntry.file)" (dblClickEvent)="fileOpenEvent.emit($event.gridEntry.file)"
(contextmenu)="fileContextMenu.onContextMenu($event, gridEntry.file)" *ngFor="let gridEntry of rowEntry"
[gridEntry]="gridEntry"></app-file-grid-entry> [gridEntry]="gridEntry"></app-file-grid-entry>
</div> </div>
</div> </div>
@ -14,5 +14,5 @@
</div> </div>
<app-file-context-menu #fileContextMenu> <app-file-context-menu #fileContextMenu>
<button mat-menu-item (click)="this.regenerateThumbnail(fileContextMenu.file)">Regenerate thumbnail</button> <button (click)="this.regenerateThumbnail(fileContextMenu.file)" mat-menu-item>Regenerate thumbnail</button>
</app-file-context-menu> </app-file-context-menu>

@ -92,6 +92,16 @@ export class FileGridComponent implements OnChanges, OnInit {
this.fileSelectEvent.emit(this.selectedEntries.map(g => g.file)); this.fileSelectEvent.emit(this.selectedEntries.map(g => g.file));
} }
public adjustElementSizes(): void {
if (this.virtualScroll) {
this.virtualScroll.checkViewportSize();
}
}
public async regenerateThumbnail(file: File) {
await this.fileService.deleteThumbnails(file);
}
private setPartitionedGridEntries() { private setPartitionedGridEntries() {
this.partitionedGridEntries = []; this.partitionedGridEntries = [];
let scrollToIndex = -1; let scrollToIndex = -1;
@ -178,14 +188,4 @@ export class FileGridComponent implements OnChanges, OnInit {
break; break;
} }
} }
public adjustElementSizes(): void {
if (this.virtualScroll) {
this.virtualScroll.checkViewportSize();
}
}
public async regenerateThumbnail(file: File) {
await this.fileService.deleteThumbnails(file);
}
} }

@ -1,4 +1,5 @@
<app-file-grid *ngIf="this.mode === 'grid'" [files]="this.files" [preselectedFile]="this.preselectedFile" <app-file-grid (fileOpenEvent)="this.onFileOpen($event)" (fileSelectEvent)="this.onFileSelect($event)" *ngIf="this.mode === 'grid'"
(fileSelectEvent)="this.onFileSelect($event)" (fileOpenEvent)="this.onFileOpen($event)"></app-file-grid> [files]="this.files" [preselectedFile]="this.preselectedFile"></app-file-grid>
<app-file-gallery *ngIf="this.mode === 'gallery'" [files]="this.files" [preselectedFile]="this.preselectedFile" <app-file-gallery (closeEvent)="this.mode = 'grid'" (fileSelectEvent)="this.onSinglefileSelect($event)" *ngIf="this.mode === 'gallery'"
(fileSelectEvent)="this.onSinglefileSelect($event)" (closeEvent)="this.mode = 'grid'"></app-file-gallery> [files]="this.files"
[preselectedFile]="this.preselectedFile"></app-file-gallery>

@ -1,4 +1,4 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import {File} from "../../../models/File"; import {File} from "../../../models/File";
@Component({ @Component({
@ -17,7 +17,8 @@ export class FileMultiviewComponent {
public selectedFiles: File[] = []; public selectedFiles: File[] = [];
public preselectedFile: File | undefined; public preselectedFile: File | undefined;
constructor() { } constructor() {
}
public onFileSelect(files: File[]): void { public onFileSelect(files: File[]): void {
this.selectedFiles = files; this.selectedFiles = files;

@ -9,8 +9,9 @@
<ng-icon name="mat-delete-sweep"></ng-icon> <ng-icon name="mat-delete-sweep"></ng-icon>
</button> </button>
</div> </div>
<app-tag-input class="full-width" [allowNegation]="true" [availableTags]="getValidSearchTags()" (tagAdded)="addSearchTag($event); searchForFiles()"> <app-tag-input (tagAdded)="addSearchTag($event); searchForFiles()" [allowNegation]="true" [availableTags]="getValidSearchTags()"
<button mat-button class="filter-dialog-button" (click)="openFilterDialog()"> class="full-width">
<button (click)="openFilterDialog()" class="filter-dialog-button" mat-button>
<ng-icon name="mat-filter-alt"></ng-icon> <ng-icon name="mat-filter-alt"></ng-icon>
</button> </button>
</app-tag-input> </app-tag-input>

@ -43,7 +43,8 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
private errorBroker: ErrorBrokerService, private errorBroker: ErrorBrokerService,
private fileService: FileService, private fileService: FileService,
public dialog: MatDialog public dialog: MatDialog
) {} ) {
}
public async ngOnInit() { public async ngOnInit() {
await this.searchForFiles(); await this.searchForFiles();
@ -74,7 +75,8 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
} }
public getValidSearchTags(): Tag[] { public getValidSearchTags(): Tag[] {
return this.availableTags.filter(t => this.filters.findIndex(f => f.partiallyEq(t.getNormalizedOutput())) < 0); return this.availableTags.filter(t => this.filters.findIndex(
f => f.partiallyEq(t.getNormalizedOutput())) < 0);
} }
public async removeAllSearchTags() { public async removeAllSearchTags() {

@ -1,26 +1,26 @@
<h1 mat-dialog-title>Filters</h1> <h1 mat-dialog-title>Filters</h1>
<div mat-dialog-content class="filter-dialog-content"> <div class="filter-dialog-content" mat-dialog-content>
<div class="filter-dialog-list"> <div class="filter-dialog-list">
<mat-list class="mat-filter-dialog-list"> <mat-list class="mat-filter-dialog-list">
<mat-list-item class="filter-list-item" *ngFor="let expression of filters" [class.selected]="expression.selected"> <mat-list-item *ngFor="let expression of filters" [class.selected]="expression.selected" class="filter-list-item">
<app-tag-filter-list-item (querySelect)="this.addToSelection($event)" <app-tag-filter-list-item (contextmenu)="contextMenu.onContextMenu($event)"
(querySelect)="this.addToSelection($event)"
(queryUnselect)="this.removeFromSelection($event)" (queryUnselect)="this.removeFromSelection($event)"
(removeClicked)="this.removeFilter($event)" (removeClicked)="this.removeFilter($event)"
(contextmenu)="contextMenu.onContextMenu($event)"
[expression]="expression"></app-tag-filter-list-item> [expression]="expression"></app-tag-filter-list-item>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</div> </div>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<app-tag-input class="tag-input" [allowNegation]="true" [availableTags]="this.availableTags" <app-tag-input (tagAdded)="this.addFilter($event)" [allowNegation]="true" [availableTags]="this.availableTags"
(tagAdded)="this.addFilter($event)"></app-tag-input> class="tag-input"></app-tag-input>
</div> </div>
<div class="dialog-actions" mat-dialog-actions> <div class="dialog-actions" mat-dialog-actions>
<button mat-flat-button color="primary" (click)="confirmFilter()">Filter</button> <button (click)="confirmFilter()" color="primary" mat-flat-button>Filter</button>
<button mat-stroked-button color="accent" (click)="cancelFilter()">Cancel</button> <button (click)="cancelFilter()" color="accent" mat-stroked-button>Cancel</button>
</div> </div>
<app-context-menu #contextMenu> <app-context-menu #contextMenu>
<button mat-menu-item (click)="this.convertSelectionToOrExpression()">Copy to OR-Expression</button> <button (click)="this.convertSelectionToOrExpression()" mat-menu-item>Copy to OR-Expression</button>
<button mat-menu-item (click)="this.convertSelectionToAndExpression()">Copy to AND-Expression</button> <button (click)="this.convertSelectionToAndExpression()" mat-menu-item>Copy to AND-Expression</button>
<button mat-menu-item (click)="this.invertSelection()">Invert</button> <button (click)="this.invertSelection()" mat-menu-item>Invert</button>
</app-context-menu> </app-context-menu>

@ -35,6 +35,24 @@ export class FilterDialogComponent {
this.availableTags = data.availableTags ?? []; this.availableTags = data.availableTags ?? [];
} }
private static checkFiltersEqual(l: FilterExpression, r: FilterExpression): boolean {
const lTags = l.queryList().map(q => q.getNormalizedTag()).sort();
const rTags = r.queryList().map(q => q.getNormalizedTag()).sort();
let match = false;
if (lTags.length == rTags.length) {
match = true;
for (const tag of lTags) {
match = rTags.includes(tag);
if (!match) {
break;
}
}
}
return match;
}
public cancelFilter(): void { public cancelFilter(): void {
this.dialogRef.close(); this.dialogRef.close();
} }
@ -97,7 +115,9 @@ export class FilterDialogComponent {
public convertSelectionToAndExpression(): void { public convertSelectionToAndExpression(): void {
for (const query of this.selectedQueries) { for (const query of this.selectedQueries) {
this.filters.push(new Selectable<FilterExpression>(new SingleFilterExpression(query), false)); this.filters.push(
new Selectable<FilterExpression>(new SingleFilterExpression(query),
false));
} }
this.removeFilterDuplicates(); this.removeFilterDuplicates();
this.unselectAll(); this.unselectAll();
@ -123,9 +143,12 @@ export class FilterDialogComponent {
if (filterItem.data.filter_type == "OrExpression") { if (filterItem.data.filter_type == "OrExpression") {
(filterItem.data as OrFilterExpression).removeDuplicates(); (filterItem.data as OrFilterExpression).removeDuplicates();
} }
if (newFilters.findIndex(f => FilterDialogComponent.checkFiltersEqual(f.data, filterItem.data)) < 0) { if (newFilters.findIndex(
f => FilterDialogComponent.checkFiltersEqual(f.data,
filterItem.data)) < 0) {
if (filterItem.data.filter_type == "OrExpression" && filterItem.data.queryList().length === 1) { if (filterItem.data.filter_type == "OrExpression" && filterItem.data.queryList().length === 1) {
filterItem.data = new SingleFilterExpression(filterItem.data.queryList()[0]); filterItem.data = new SingleFilterExpression(
filterItem.data.queryList()[0]);
} }
newFilters.push(filterItem); newFilters.push(filterItem);
} }
@ -133,24 +156,6 @@ export class FilterDialogComponent {
this.filters = newFilters; this.filters = newFilters;
} }
private static checkFiltersEqual(l: FilterExpression, r: FilterExpression): boolean {
const lTags = l.queryList().map(q => q.getNormalizedTag()).sort();
const rTags = r.queryList().map(q => q.getNormalizedTag()).sort();
let match = false;
if (lTags.length == rTags.length) {
match = true;
for (const tag of lTags) {
match = rTags.includes(tag);
if (!match) {
break;
}
}
}
return match;
}
@HostListener("window:keydown", ["$event"]) @HostListener("window:keydown", ["$event"])
private async handleKeydownEvent(event: KeyboardEvent) { private async handleKeydownEvent(event: KeyboardEvent) {
if (event.key === "Shift") { if (event.key === "Shift") {

@ -1,16 +1,18 @@
<div *ngIf="expression.data.filter_type === 'Query'" (click)="onSelect()"> <div (click)="onSelect()" *ngIf="expression.data.filter_type === 'Query'">
{{expression.data.getDisplayName()}} {{expression.data.getDisplayName()}}
<button mat-button class="remove-button" (click)="this.removeClicked.emit(this)"> <button (click)="this.removeClicked.emit(this)" class="remove-button" mat-button>
<ng-icon name="mat-remove"></ng-icon> <ng-icon name="mat-remove"></ng-icon>
</button> </button>
</div> </div>
<div *ngIf="expression.data.filter_type === 'OrExpression'"> <div *ngIf="expression.data.filter_type === 'OrExpression'">
<mat-list> <mat-list>
<mat-list-item class="or-filter-list-item" *ngFor="let entry of enumerate(this.expression.data.queryList())" <mat-list-item (mousedown)="$event.button === 0 && this.selectInnerIndex(entry[0])" *ngFor="let entry of enumerate(this.expression.data.queryList())"
(mousedown)="$event.button === 0 && this.selectInnerIndex(entry[0])" [class.selected]="this.selectedIndices.includes(entry[0])"> [class.selected]="this.selectedIndices.includes(entry[0])"
<span class="or-span" *ngIf="entry[0] > 0">OR</span> class="or-filter-list-item">
<span *ngIf="entry[0] > 0" class="or-span">OR</span>
{{entry[1].getNormalizedTag()}} {{entry[1].getNormalizedTag()}}
<button mat-button class="remove-button-inner-list" (mousedown)="$event.button === 0 && this.removeOrExpression(entry[0])"> <button (mousedown)="$event.button === 0 && this.removeOrExpression(entry[0])" class="remove-button-inner-list"
mat-button>
<ng-icon name="mat-remove"></ng-icon> <ng-icon name="mat-remove"></ng-icon>
</button> </button>
</mat-list-item> </mat-list-item>

@ -2,14 +2,15 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
EventEmitter, EventEmitter,
Inject, Input,
Input, OnChanges, OnChanges,
OnInit, Output,
Output, SimpleChanges SimpleChanges
} from '@angular/core'; } from '@angular/core';
import { import {
FilterExpression, FilterExpression,
OrFilterExpression, SingleFilterExpression OrFilterExpression,
SingleFilterExpression
} from "../../../../../models/FilterExpression"; } from "../../../../../models/FilterExpression";
import {TagQuery} from "../../../../../models/TagQuery"; import {TagQuery} from "../../../../../models/TagQuery";
import {Selectable} from "../../../../../models/Selectable"; import {Selectable} from "../../../../../models/Selectable";
@ -28,7 +29,8 @@ export class TagFilterListItemComponent implements OnChanges {
public selectedIndices: number[] = []; public selectedIndices: number[] = [];
constructor(private changeDetector: ChangeDetectorRef) { } constructor(private changeDetector: ChangeDetectorRef) {
}
public ngOnChanges(changes: SimpleChanges): void { public ngOnChanges(changes: SimpleChanges): void {
if (changes["expression"]) { if (changes["expression"]) {

@ -21,7 +21,8 @@
</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 (change)="sortKey.namespaceName = namespaceInput.value" [value]="sortKey.namespaceName ?? ''" matInput <input #namespaceInput (change)="sortKey.namespaceName = namespaceInput.value"
[value]="sortKey.namespaceName ?? ''" matInput
required> required>
</mat-form-field> </mat-form-field>
<div *ngIf="sortKey.sortType !== 'Namespace'" class="filler"></div> <div *ngIf="sortKey.sortType !== 'Namespace'" class="filler"></div>

@ -1,10 +1,10 @@
<app-content-aware-image borderRadius="0.25em" *ngIf="this.getThumbnailSupported() && this.thumbUrl" <app-content-aware-image *ngIf="this.getThumbnailSupported() && this.thumbUrl" [imageSrc]="this.thumbUrl"
[imageSrc]="this.thumbUrl"></app-content-aware-image> borderRadius="0.25em"></app-content-aware-image>
<div class="file-icon-overlay" *ngIf="this.getThumbnailSupported() && this.thumbUrl"> <div *ngIf="this.getThumbnailSupported() && this.thumbUrl" class="file-icon-overlay">
<ng-icon *ngIf="getFileType() === 'video'" name="mat-movie"></ng-icon> <ng-icon *ngIf="getFileType() === 'video'" name="mat-movie"></ng-icon>
<ng-icon *ngIf="this.file.mime_type === 'image/gif'" class="gif-icon" name="mat-gif"></ng-icon> <ng-icon *ngIf="this.file.mime_type === 'image/gif'" class="gif-icon" name="mat-gif"></ng-icon>
</div> </div>
<div class="file-type-icon" *ngIf="!this.getThumbnailSupported() || !this.thumbUrl"> <div *ngIf="!this.getThumbnailSupported() || !this.thumbUrl" class="file-type-icon">
<ng-icon *ngIf="getFileType() === 'image'" name="mat-image"></ng-icon> <ng-icon *ngIf="getFileType() === 'image'" name="mat-image"></ng-icon>
<ng-icon *ngIf="getFileType() === 'video'" name="mat-movie"></ng-icon> <ng-icon *ngIf="getFileType() === 'video'" name="mat-movie"></ng-icon>
<ng-icon *ngIf="getFileType() === 'audio'" name="mat-audiotrack"></ng-icon> <ng-icon *ngIf="getFileType() === 'audio'" name="mat-audiotrack"></ng-icon>

@ -1,4 +1,10 @@
import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core'; import {
Component,
Input,
OnChanges,
OnInit,
SimpleChanges
} 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 {FileHelper} from "../../../services/file/file.helper"; import {FileHelper} from "../../../services/file/file.helper";

@ -2,13 +2,14 @@
<mat-form-field appearance="fill"> <mat-form-field appearance="fill">
<mat-label>{{label}}</mat-label> <mat-label>{{label}}</mat-label>
<input #filesInput matInput [value]="files.join(', ')" (change)="this.setFiles(filesInput.value)" class="file-input"> <input #filesInput (change)="this.setFiles(filesInput.value)" [value]="files.join(', ')" class="file-input"
matInput>
<div class="buttons-native-select"> <div class="buttons-native-select">
<button *ngIf="mode === 'files'" (click)="openNativeFileSelectDialog(false)" mat-button> <button (click)="openNativeFileSelectDialog(false)" *ngIf="mode === 'files'" mat-button>
<ng-icon name="mat-insert-drive-file"></ng-icon> <ng-icon name="mat-insert-drive-file"></ng-icon>
</button> </button>
<button *ngIf="mode === 'folders'" (click)="openNativeFileSelectDialog(true)" mat-button> <button (click)="openNativeFileSelectDialog(true)" *ngIf="mode === 'folders'" mat-button>
<ng-icon name="mat-folder"></ng-icon> <ng-icon name="mat-folder"></ng-icon>
</button> </button>
</div> </div>

@ -3,10 +3,10 @@
Enter a tag Enter a tag
</mat-label> </mat-label>
<input #tagInput <input #tagInput
[formControl]="formControl"
matInput
(keydown.enter)="addTagByInput($event)" (keydown.enter)="addTagByInput($event)"
[matAutocomplete]="auto"> [formControl]="formControl"
[matAutocomplete]="auto"
matInput>
<ng-content></ng-content> <ng-content></ng-content>
<mat-autocomplete #auto (optionSelected)="addTagByAutocomplete($event)"> <mat-autocomplete #auto (optionSelected)="addTagByAutocomplete($event)">
<mat-option *ngFor="let tag of autosuggestTags | async" [value]="tag"> <mat-option *ngFor="let tag of autosuggestTags | async" [value]="tag">

@ -1,16 +1,18 @@
import { import {
Component, ElementRef, Component,
ElementRef,
EventEmitter, EventEmitter,
Input, OnChanges, Input,
OnInit, OnChanges,
Output, SimpleChanges, Output,
SimpleChanges,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import {Tag} from "../../../../models/Tag"; import {Tag} from "../../../../models/Tag";
import {FormControl} from "@angular/forms"; import {FormControl} from "@angular/forms";
import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete"; import {MatAutocompleteSelectedEvent} from "@angular/material/autocomplete";
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {debounceTime, delay, map, startWith} from "rxjs/operators"; import {debounceTime, map, startWith} from "rxjs/operators";
@Component({ @Component({
selector: 'app-tag-input', selector: 'app-tag-input',
@ -30,16 +32,19 @@ export class TagInputComponent implements OnChanges{
private tagsForAutocomplete: string[] = []; private tagsForAutocomplete: string[] = [];
constructor() { constructor() {
this.tagsForAutocomplete = this.availableTags.map(t => t.getNormalizedOutput()); this.tagsForAutocomplete = this.availableTags.map(
t => t.getNormalizedOutput());
this.autosuggestTags = this.formControl.valueChanges.pipe( this.autosuggestTags = this.formControl.valueChanges.pipe(
startWith(null), startWith(null),
debounceTime(250), debounceTime(250),
map((tag: string | null) => tag ? this.filterSuggestionTag(tag) : this.tagsForAutocomplete.slice(0, 20))); map((tag: string | null) => tag ? this.filterSuggestionTag(
tag) : this.tagsForAutocomplete.slice(0, 20)));
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes["availableTags"]) { if (changes["availableTags"]) {
this.tagsForAutocomplete = this.availableTags.map(t => t.getNormalizedOutput()); this.tagsForAutocomplete = this.availableTags.map(
t => t.getNormalizedOutput());
} }
} }
@ -63,7 +68,8 @@ export class TagInputComponent implements OnChanges{
private filterSuggestionTag(tag: string) { private filterSuggestionTag(tag: string) {
let normalizedTag = this.normalizeTag(tag); let normalizedTag = this.normalizeTag(tag);
const negated = normalizedTag.startsWith("-") && this.allowNegation; const negated = normalizedTag.startsWith("-") && this.allowNegation;
normalizedTag = this.allowNegation? normalizedTag.replace(/^-/, "") : normalizedTag; normalizedTag = this.allowNegation ? normalizedTag.replace(/^-/,
"") : normalizedTag;
return this.tagsForAutocomplete.filter( return this.tagsForAutocomplete.filter(
t => t.includes(normalizedTag)) t => t.includes(normalizedTag))

@ -1,6 +1,5 @@
import {NgModule} from '@angular/core'; import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {AppComponent} from "../../app.component";
import {RepositoryCardComponent} from "../core/repositories-tab/repository-card/repository-card.component"; import {RepositoryCardComponent} from "../core/repositories-tab/repository-card/repository-card.component";
import {FileGridComponent} from "./file-multiview/file-grid/file-grid.component"; import {FileGridComponent} from "./file-multiview/file-grid/file-grid.component";
import {FileGridEntryComponent} from "./file-multiview/file-grid/file-grid-entry/file-grid-entry.component"; import {FileGridEntryComponent} from "./file-multiview/file-grid/file-grid-entry/file-grid-entry.component";
@ -60,7 +59,6 @@ import {NgIconsModule} from "@ng-icons/core";
import * as materialIcons from "@ng-icons/material-icons"; import * as materialIcons from "@ng-icons/material-icons";
@NgModule({ @NgModule({
declarations: [ declarations: [
RepositoryCardComponent, RepositoryCardComponent,
@ -135,4 +133,5 @@ import * as materialIcons from "@ng-icons/material-icons";
NgIconsModule.withIcons({...materialIcons}), NgIconsModule.withIcons({...materialIcons}),
] ]
}) })
export class SharedModule { } export class SharedModule {
}

@ -82,7 +82,8 @@ export class SingleFilterExpression implements FilterExpression {
} }
public clone(): FilterExpression { public clone(): FilterExpression {
return new SingleFilterExpression(new TagQuery(this.filter.tag, this.filter.negate)) return new SingleFilterExpression(
new TagQuery(this.filter.tag, this.filter.negate))
} }
public queryList(): TagQuery[] { public queryList(): TagQuery[] {

@ -1,5 +1,3 @@
import {SingleFilterExpression} from "./FilterExpression";
export class TagQuery { export class TagQuery {
constructor(public tag: string, public negate: boolean) { constructor(public tag: string, public negate: boolean) {
} }

@ -41,7 +41,8 @@ export class FileService {
} }
public async updateFileName(file: File, name: string): Promise<File> { public async updateFileName(file: File, name: string): Promise<File> {
return await invoke<File>("plugin:mediarepo|update_file_name", {id: file.id, name}) return await invoke<File>("plugin:mediarepo|update_file_name",
{id: file.id, name})
} }
/** /**
@ -52,7 +53,8 @@ export class FileService {
* @returns {SafeResourceUrl} * @returns {SafeResourceUrl}
*/ */
public buildThumbnailUrl(file: File, height: number, width: number): SafeResourceUrl { public buildThumbnailUrl(file: File, height: number, width: number): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(`thumb://${file.hash}?width=${250}&height=${250}`) return this.sanitizer.bypassSecurityTrustResourceUrl(
`thumb://${file.hash}?width=${250}&height=${250}`)
} }
/** /**
@ -61,7 +63,8 @@ export class FileService {
* @returns {SafeResourceUrl} * @returns {SafeResourceUrl}
*/ */
public buildContentUrl(file: File): SafeResourceUrl { public buildContentUrl(file: File): SafeResourceUrl {
return this.sanitizer.bypassSecurityTrustResourceUrl(`content://${file.hash}`) return this.sanitizer.bypassSecurityTrustResourceUrl(
`content://${file.hash}`)
} }
/** /**
@ -71,7 +74,8 @@ export class FileService {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async saveFile(file: File, targetPath: string) { public async saveFile(file: File, targetPath: string) {
await invoke("plugin:mediarepo|save_file_locally", {id: file.id, path: targetPath}) await invoke("plugin:mediarepo|save_file_locally",
{id: file.id, path: targetPath})
} }
/** /**
@ -89,7 +93,8 @@ export class FileService {
* @returns {Promise<SafeResourceUrl>} * @returns {Promise<SafeResourceUrl>}
*/ */
public async readFile(file: File): Promise<SafeResourceUrl> { public async readFile(file: File): Promise<SafeResourceUrl> {
const data = await invoke<number[]>("plugin:mediarepo|read_file", {hash: file.hash, mimeType: file.mime_type}); const data = await invoke<number[]>("plugin:mediarepo|read_file",
{hash: file.hash, mimeType: file.mime_type});
const blob = new Blob([new Uint8Array(data)], {type: file.mime_type}); const blob = new Blob([new Uint8Array(data)], {type: file.mime_type});
const url = URL?.createObjectURL(blob); const url = URL?.createObjectURL(blob);
return this.sanitizer.bypassSecurityTrustResourceUrl(url); return this.sanitizer.bypassSecurityTrustResourceUrl(url);

@ -9,7 +9,8 @@ import {File} from "../../models/File";
}) })
export class ImportService { export class ImportService {
constructor() { } constructor() {
}
/** /**
* Resolves paths from the local file system into a list of files that can be imported * Resolves paths from the local file system into a list of files that can be imported
@ -17,7 +18,8 @@ export class ImportService {
* @returns {Promise<FileOsMetadata[]>} * @returns {Promise<FileOsMetadata[]>}
*/ */
public async resolvePathsToFiles(paths: string[]): Promise<FileOsMetadata[]> { public async resolvePathsToFiles(paths: string[]): Promise<FileOsMetadata[]> {
return await invoke<FileOsMetadata[]>("plugin:mediarepo|resolve_paths_to_files", {paths}); return await invoke<FileOsMetadata[]>(
"plugin:mediarepo|resolve_paths_to_files", {paths});
} }
/** /**
@ -27,6 +29,7 @@ export class ImportService {
* @returns {Promise<File>} * @returns {Promise<File>}
*/ */
public async addLocalFile(metadata: FileOsMetadata, options: AddFileOptions): Promise<File> { public async addLocalFile(metadata: FileOsMetadata, options: AddFileOptions): Promise<File> {
return await invoke<File>("plugin:mediarepo|add_local_file", {metadata, options}); return await invoke<File>("plugin:mediarepo|add_local_file",
{metadata, options});
} }
} }

@ -7,7 +7,9 @@ import {BehaviorSubject} from "rxjs";
export class TabService { export class TabService {
public selectedTab = new BehaviorSubject<number>(0); public selectedTab = new BehaviorSubject<number>(0);
constructor() { }
constructor() {
}
public setSelectedTab(index: number) { public setSelectedTab(index: number) {
this.selectedTab.next(index); this.selectedTab.next(index);

@ -2,7 +2,6 @@ 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} from "rxjs"; import {BehaviorSubject} from "rxjs";
import {File} from "../../models/File";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -29,12 +28,14 @@ export class TagService {
} }
public async createTags(tags: string[]): Promise<Tag[]> { public async createTags(tags: string[]): Promise<Tag[]> {
const resultTags = await invoke<Tag[]>("plugin:mediarepo|create_tags", {tags}); const resultTags = await invoke<Tag[]>("plugin:mediarepo|create_tags",
{tags});
return resultTags.map(t => new Tag(t.id, t.name, t.namespace)); return resultTags.map(t => new Tag(t.id, t.name, t.namespace));
} }
public async changeFileTags(fileId: number, addedTags: number[], removedTags: number[]): Promise<Tag[]> { public async changeFileTags(fileId: number, addedTags: number[], removedTags: number[]): Promise<Tag[]> {
const tags = await invoke<Tag[]>("plugin:mediarepo|change_file_tags", {id: fileId, addedTags, removedTags}); const tags = await invoke<Tag[]>("plugin:mediarepo|change_file_tags",
{id: fileId, addedTags, removedTags});
return tags.map(t => new Tag(t.id, t.name, t.namespace)); return tags.map(t => new Tag(t.id, t.name, t.namespace));
} }
} }

Loading…
Cancel
Save