Change indentation to 4 spaces (because why was it 2 anyway?)

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

@ -4,12 +4,12 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 4
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.ts] [*.ts]
quote_type = single quote_type = double
[*.md] [*.md]
max_line_length = off max_line_length = off

@ -34,7 +34,9 @@
"prefix": "app", "prefix": "app",
"style": "kebab-case" "style": "kebab-case"
} }
] ],
"quotes": ["warn", "double", {"avoidEscape": true}],
"indent": ["error", 4, {"SwitchCase": 1}]
} }
}, },
{ {

@ -1,13 +1,13 @@
import {NgModule} from '@angular/core'; import {NgModule} from "@angular/core";
import {RouterModule, Routes} from '@angular/router'; import {RouterModule, Routes} from "@angular/router";
import {CoreComponent} from "./components/core/core.component"; import {CoreComponent} from "./components/core/core.component";
const routes: Routes = [ const routes: Routes = [
{path: "", component: CoreComponent}]; {path: "", component: CoreComponent}];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AppRoutingModule { export class AppRoutingModule {
} }

@ -2,53 +2,53 @@
@use '~@angular/material' as mat; @use '~@angular/material' as mat;
@mixin color($theme) { @mixin color($theme) {
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);
$primary-palette: map.get($color-config, 'primary'); $primary-palette: map.get($color-config, 'primary');
$warn-palette: map.get($color-config, 'warn'); $warn-palette: map.get($color-config, 'warn');
body { body {
background-color: darken(#303030, 5); background-color: darken(#303030, 5);
color: white color: white
} }
::ng-deep ::-webkit-scrollbar { ::ng-deep ::-webkit-scrollbar {
width: 15px; width: 15px;
height: 15px; height: 15px;
background: #272727; background: #272727;
} }
::ng-deep ::-webkit-scrollbar-thumb { ::ng-deep ::-webkit-scrollbar-thumb {
background: mat.get-color-from-palette($primary-palette, 'darker'); background: mat.get-color-from-palette($primary-palette, 'darker');
border-radius: 1px; border-radius: 1px;
} }
::ng-deep ::-webkit-scrollbar-thumb:hover { ::ng-deep ::-webkit-scrollbar-thumb:hover {
background: mat.get-color-from-palette($primary-palette); background: mat.get-color-from-palette($primary-palette);
} }
.warn { .warn {
background-color: mat.get-color-from-palette($warn-palette); background-color: mat.get-color-from-palette($warn-palette);
color: white color: white
} }
} }
@mixin typography($theme) { @mixin typography($theme) {
// Get the typography config from the theme. // Get the typography config from the theme.
$typography-config: mat.get-typography-config($theme); $typography-config: mat.get-typography-config($theme);
body { body {
font-family: mat.font-family($typography-config); font-family: mat.font-family($typography-config);
} }
} }
@mixin theme($theme) { @mixin theme($theme) {
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);
@if $color-config != null { @if $color-config != null {
@include color($theme); @include color($theme);
} }
$typography-config: mat.get-typography-config($theme); $typography-config: mat.get-typography-config($theme);
@if $typography-config != null { @if $typography-config != null {
@include typography($theme); @include typography($theme);
} }
} }

@ -1,7 +1,7 @@
::ng-deep .mat-button-wrapper > ng-icon { ::ng-deep .mat-button-wrapper > ng-icon {
font-size: 26px; font-size: 26px;
} }
::ng-deep ng-icon { ::ng-deep ng-icon {
font-size: 24px; font-size: 24px;
} }

@ -1,36 +1,36 @@
import {TestBed} from '@angular/core/testing'; import {TestBed} from "@angular/core/testing";
import {RouterTestingModule} from '@angular/router/testing'; import {RouterTestingModule} from "@angular/router/testing";
import {AppComponent} from './app.component'; import {AppComponent} from "./app.component";
describe('AppComponent', () => { describe("AppComponent", () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [ imports: [
RouterTestingModule RouterTestingModule
], ],
declarations: [ declarations: [
AppComponent AppComponent
], ],
}).compileComponents(); }).compileComponents();
}); });
it('should create the app', () => { it("should create the app", () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app).toBeTruthy(); expect(app).toBeTruthy();
}); });
it(`should have as title 'mediarepo-ui'`, () => { it("should have as title 'mediarepo-ui'", () => {
const fixture = TestBed.createComponent(AppComponent); const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance; const app = fixture.componentInstance;
expect(app.title).toEqual('mediarepo-ui'); expect(app.title).toEqual("mediarepo-ui");
}); });
it('should render title', () => { it("should render title", () => {
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) expect(compiled.querySelector(".content span")?.textContent)
.toContain('mediarepo-ui app is running!'); .toContain("mediarepo-ui app is running!");
}); });
}); });

@ -1,43 +1,43 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from "@angular/core";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
import {RepositoryService} from "./services/repository/repository.service"; import {RepositoryService} from "./services/repository/repository.service";
import {MatSnackBar} from "@angular/material/snack-bar"; import {MatSnackBar} from "@angular/material/snack-bar";
import {ErrorBrokerService} from "./services/error-broker/error-broker.service"; import {ErrorBrokerService} from "./services/error-broker/error-broker.service";
@Component({ @Component({
selector: 'app-root', selector: "app-root",
templateUrl: './app.component.html', templateUrl: "./app.component.html",
styleUrls: ['./app.component.scss'] styleUrls: ["./app.component.scss"]
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
title = 'mediarepo-ui'; title = "mediarepo-ui";
constructor( constructor(
private router: Router, private router: Router,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private errorBroker: ErrorBrokerService, private errorBroker: ErrorBrokerService,
private repoService: RepositoryService, private repoService: RepositoryService,
) { ) {
} }
async ngOnInit() { async ngOnInit() {
this.errorBroker.errorCb = (err: { message: string }) => this.showError( this.errorBroker.errorCb = (err: { message: string }) => this.showError(
err); 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();
} }
private showError(err: { message: string }) { private showError(err: { message: string }) {
this.snackBar.open(err.message, undefined, { this.snackBar.open(err.message, undefined, {
panelClass: "warn", panelClass: "warn",
duration: 2000, duration: 2000,
}); });
} }
private showInfo(info: string) { private showInfo(info: string) {
this.snackBar.open(info, undefined, { this.snackBar.open(info, undefined, {
panelClass: "primary", panelClass: "primary",
duration: 2000, duration: 2000,
}); });
} }
} }

@ -1,20 +1,20 @@
import {NgModule} from '@angular/core'; import {NgModule} from "@angular/core";
import {BrowserModule} from '@angular/platform-browser'; import {BrowserModule} from "@angular/platform-browser";
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 {CoreModule} from "./components/core/core.module"; import {CoreModule} from "./components/core/core.module";
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent, AppComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule, BrowserAnimationsModule,
CoreModule, CoreModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { export class AppModule {
} }

@ -1,13 +1,13 @@
<div id="content"> <div id="content">
<mat-tab-group #tabGroup (selectedTabChange)="this.onTabSelectionChange($event)"> <mat-tab-group #tabGroup (selectedTabChange)="this.onTabSelectionChange($event)">
<mat-tab label="Repositories"> <mat-tab label="Repositories">
<app-repositories-tab></app-repositories-tab> <app-repositories-tab></app-repositories-tab>
</mat-tab> </mat-tab>
<mat-tab *ngIf="this.selectedRepository" label="Files"> <mat-tab *ngIf="this.selectedRepository" label="Files">
<app-files-tab></app-files-tab> <app-files-tab></app-files-tab>
</mat-tab> </mat-tab>
<mat-tab *ngIf="this.selectedRepository" label="Import"> <mat-tab *ngIf="this.selectedRepository" label="Import">
<app-import-tab></app-import-tab> <app-import-tab></app-import-tab>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>

@ -1,19 +1,19 @@
#content { #content {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
margin: 0; margin: 0;
overflow: hidden overflow: hidden
} }
mat-tab-group { mat-tab-group {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
::ng-deep .mat-tab-body-wrapper { ::ng-deep .mat-tab-body-wrapper {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {CoreComponent} from './core.component'; import {CoreComponent} from "./core.component";
describe('HomeComponent', () => { describe("HomeComponent", () => {
let component: CoreComponent; let component: CoreComponent;
let fixture: ComponentFixture<CoreComponent>; let fixture: ComponentFixture<CoreComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [CoreComponent] declarations: [CoreComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(CoreComponent); fixture = TestBed.createComponent(CoreComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,4 +1,4 @@
import {Component, OnInit, ViewChild} from '@angular/core'; import {Component, OnInit, ViewChild} from "@angular/core";
import {Repository} from "../../models/Repository"; import {Repository} from "../../models/Repository";
import {RepositoryService} from "../../services/repository/repository.service"; import {RepositoryService} from "../../services/repository/repository.service";
import {MatTabChangeEvent, MatTabGroup} from "@angular/material/tabs"; import {MatTabChangeEvent, MatTabGroup} from "@angular/material/tabs";
@ -6,48 +6,48 @@ import {TagService} from "../../services/tag/tag.service";
import {TabService} from "../../services/tab/tab.service"; import {TabService} from "../../services/tab/tab.service";
@Component({ @Component({
selector: 'app-core', selector: "app-core",
templateUrl: './core.component.html', templateUrl: "./core.component.html",
styleUrls: ['./core.component.scss'] styleUrls: ["./core.component.scss"]
}) })
export class CoreComponent implements OnInit { export class CoreComponent implements OnInit {
public selectedRepository: Repository | undefined; public selectedRepository: Repository | undefined;
@ViewChild("tabGroup") tabGroup!: MatTabGroup; @ViewChild("tabGroup") tabGroup!: MatTabGroup;
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> {
this.selectedRepository = this.repoService.selectedRepository.getValue();
this.repoService.selectedRepository.subscribe(async (selected) => {
this.selectedRepository = selected;
this.updateSelectedTab();
await this.loadRepoData();
});
}
public updateSelectedTab() { public async ngOnInit(): Promise<void> {
if (!this.tabGroup) { this.selectedRepository = this.repoService.selectedRepository.getValue();
return; this.repoService.selectedRepository.subscribe(async (selected) => {
this.selectedRepository = selected;
this.updateSelectedTab();
await this.loadRepoData();
});
} }
if (!this.selectedRepository) {
this.tabGroup.selectedIndex = 0; public updateSelectedTab() {
} else if (this.tabGroup.selectedIndex === 0) { if (!this.tabGroup) {
this.tabGroup.selectedIndex = 1; return;
}
if (!this.selectedRepository) {
this.tabGroup.selectedIndex = 0;
} else if (this.tabGroup.selectedIndex === 0) {
this.tabGroup.selectedIndex = 1;
}
} }
}
async loadRepoData() { async loadRepoData() {
await this.tagService.loadTags(); await this.tagService.loadTags();
} }
public onTabSelectionChange(event: MatTabChangeEvent): void { public onTabSelectionChange(event: MatTabChangeEvent): void {
this.tabService.setSelectedTab(event.index); this.tabService.setSelectedTab(event.index);
} }
} }

@ -1,5 +1,5 @@
import {NgModule} from '@angular/core'; import {NgModule} from "@angular/core";
import {CommonModule} from '@angular/common'; import {CommonModule} from "@angular/common";
import {CoreComponent} from "./core.component"; import {CoreComponent} from "./core.component";
import {RepositoriesTabComponent} from "./repositories-tab/repositories-tab.component"; import {RepositoriesTabComponent} from "./repositories-tab/repositories-tab.component";
import {FilesTabComponent} from "./files-tab/files-tab.component"; import {FilesTabComponent} from "./files-tab/files-tab.component";
@ -24,35 +24,35 @@ import {MatMenuModule} from "@angular/material/menu";
@NgModule({ @NgModule({
declarations: [ declarations: [
RepositoriesTabComponent, RepositoriesTabComponent,
CoreComponent, CoreComponent,
FilesTabComponent, FilesTabComponent,
FilesTabSidebarComponent, FilesTabSidebarComponent,
ImportTabComponent, ImportTabComponent,
ImportTabSidebarComponent, ImportTabSidebarComponent,
FilesystemImportComponent, FilesystemImportComponent,
], ],
exports: [ exports: [
CoreComponent CoreComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
MatTabsModule, MatTabsModule,
MatSidenavModule, MatSidenavModule,
MatOptionModule, MatOptionModule,
MatSelectModule, MatSelectModule,
MatDividerModule, MatDividerModule,
MatProgressBarModule, MatProgressBarModule,
MatCheckboxModule, MatCheckboxModule,
ScrollingModule, ScrollingModule,
NgIconsModule.withIcons({...materialIcons}), NgIconsModule.withIcons({...materialIcons}),
FlexModule, FlexModule,
MatButtonModule, MatButtonModule,
MatMenuModule, MatMenuModule,
MatRippleModule, MatRippleModule,
] ]
}) })
export class CoreModule { export class CoreModule {
} }

@ -1,41 +1,43 @@
<div class="sidebar-inner"> <div class="sidebar-inner">
<mat-tab-group headerPosition="below"> <mat-tab-group headerPosition="below">
<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 (searchEndEvent)="this.searchEndEvent.emit()" <app-file-search #filesearch (searchEndEvent)="this.searchEndEvent.emit()"
(searchStartEvent)="this.searchStartEvent.emit()" (searchStartEvent)="this.searchStartEvent.emit()"
[availableTags]="this.allTags"></app-file-search> [availableTags]="this.allTags"></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">
<h2>Tags</h2> <h2>Tags</h2>
<mat-divider></mat-divider> <mat-divider></mat-divider>
</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)" (contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)" *cdkVirtualFor="let tag of tags" class="selectable-tag" <div (click)="addSearchTag(tag)"
matRipple> (contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)"
<app-tag-item [tag]="tag"></app-tag-item> *cdkVirtualFor="let tag of tags" class="selectable-tag"
matRipple>
<app-tag-item [tag]="tag"></app-tag-item>
</div>
</cdk-virtual-scroll-viewport>
</div>
</div> </div>
</cdk-virtual-scroll-viewport> </mat-tab>
</div> <mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit">
</div> <app-file-edit #fileedit [files]="this.selectedFiles"></app-file-edit>
</mat-tab> </mat-tab>
<mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit"> </mat-tab-group>
<app-file-edit #fileedit [files]="this.selectedFiles"></app-file-edit>
</mat-tab>
</mat-tab-group>
</div> </div>
<app-context-menu #contextMenu> <app-context-menu #contextMenu>
<button (click)="this.copyToClipboard(this.contextMenuTag!.getNormalizedOutput())" *ngIf="this.contextMenuTag" <button (click)="this.copyToClipboard(this.contextMenuTag!.getNormalizedOutput())" *ngIf="this.contextMenuTag"
mat-menu-item>Copy mat-menu-item>Copy
"{{contextMenuTag!.getNormalizedOutput()}}" "{{contextMenuTag!.getNormalizedOutput()}}"
</button> </button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.name)" *ngIf="this.contextMenuTag?.namespace" <button (click)="this.copyToClipboard(this.contextMenuTag!.name)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.name}}" mat-menu-item>Copy "{{this.contextMenuTag!.name}}"
</button> </button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.namespace!)" *ngIf="this.contextMenuTag?.namespace" <button (click)="this.copyToClipboard(this.contextMenuTag!.namespace!)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.namespace!}}" mat-menu-item>Copy "{{this.contextMenuTag!.namespace!}}"
</button> </button>
</app-context-menu> </app-context-menu>

@ -1,70 +1,70 @@
app-file-search { app-file-search {
display: block; display: block;
width: 100%; width: 100%;
} }
#file-search-input { #file-search-input {
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
mat-tab-group, mat-tab, .file-tag-list, app-file-edit { mat-tab-group, mat-tab, .file-tag-list, app-file-edit {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
mat-selection-list { mat-selection-list {
height: 100%; height: 100%;
user-select: none; user-select: none;
} }
.sidebar-inner, .search-tab-inner { .sidebar-inner, .search-tab-inner {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: block; display: block;
} }
.selectable-tag { .selectable-tag {
height: 50px; height: 50px;
display: flex; display: flex;
font-size: 1.2em; font-size: 1.2em;
cursor: pointer; cursor: pointer;
transition-duration: 0.1s; transition-duration: 0.1s;
user-select: none; user-select: none;
app-tag-item { app-tag-item {
margin: auto auto auto 0.25em; margin: auto auto auto 0.25em;
} }
} }
.selectable-tag:hover { .selectable-tag:hover {
background-color: darken(dimgrey, 10); background-color: darken(dimgrey, 10);
} }
.selectable-tag:active { .selectable-tag:active {
cursor: pointer; cursor: pointer;
} }
cdk-virtual-scroll-viewport { cdk-virtual-scroll-viewport {
height: 100%; height: 100%;
width: 100%;
overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%; width: 100%;
} overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%;
}
} }
mat-divider { mat-divider {
width: 100%; width: 100%;
} }
.tag-list-header { .tag-list-header {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
h2 { h2 {
margin: auto; margin: auto;
} }
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FilesTabSidebarComponent} from './files-tab-sidebar.component'; import {FilesTabSidebarComponent} from "./files-tab-sidebar.component";
describe('FilesTabSidebarComponent', () => { describe("FilesTabSidebarComponent", () => {
let component: FilesTabSidebarComponent; let component: FilesTabSidebarComponent;
let fixture: ComponentFixture<FilesTabSidebarComponent>; let fixture: ComponentFixture<FilesTabSidebarComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FilesTabSidebarComponent] declarations: [FilesTabSidebarComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FilesTabSidebarComponent); fixture = TestBed.createComponent(FilesTabSidebarComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,13 +1,13 @@
import { import {
Component, Component,
EventEmitter, EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
Output, Output,
SimpleChanges, 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";
@ -18,91 +18,92 @@ import {FileEditComponent} from "../../../shared/file-edit/file-edit.component";
import {clipboard} from "@tauri-apps/api"; import {clipboard} from "@tauri-apps/api";
@Component({ @Component({
selector: 'app-files-tab-sidebar', selector: "app-files-tab-sidebar",
templateUrl: './files-tab-sidebar.component.html', templateUrl: "./files-tab-sidebar.component.html",
styleUrls: ['./files-tab-sidebar.component.scss'] styleUrls: ["./files-tab-sidebar.component.scss"]
}) })
export class FilesTabSidebarComponent implements OnInit, OnChanges { export class FilesTabSidebarComponent implements OnInit, OnChanges {
@Input() selectedFiles: File[] = []; @Input() selectedFiles: File[] = [];
@Output() searchStartEvent = new EventEmitter<void>(); @Output() searchStartEvent = new EventEmitter<void>();
@Output() searchEndEvent = new EventEmitter<void>(); @Output() searchEndEvent = new EventEmitter<void>();
@ViewChild('filesearch') fileSearch!: FileSearchComponent; @ViewChild("filesearch") fileSearch!: FileSearchComponent;
@ViewChild("fileedit") fileEdit: FileEditComponent | undefined; @ViewChild("fileedit") fileEdit: FileEditComponent | undefined;
public tagsOfFiles: Tag[] = []; public tagsOfFiles: Tag[] = [];
public tags: Tag[] = []; public tags: Tag[] = [];
public allTags: Tag[] = []; public allTags: Tag[] = [];
public files: File[] = []; public files: File[] = [];
public tagsOfSelection: Tag[] = []; public tagsOfSelection: Tag[] = [];
public contextMenuTag: Tag | undefined; public contextMenuTag: Tag | undefined;
constructor(private repoService: RepositoryService, private tagService: TagService, private fileService: FileService) { constructor(private repoService: RepositoryService, private tagService: TagService, private fileService: FileService) {
this.fileService.displayedFiles.subscribe(async files => { this.fileService.displayedFiles.subscribe(async files => {
this.files = files; this.files = files;
await this.loadTagsForDisplayedFiles(); await this.loadTagsForDisplayedFiles();
await this.refreshFileSelection(); await this.refreshFileSelection();
}); });
this.repoService.selectedRepository.subscribe( this.repoService.selectedRepository.subscribe(
async (repo) => repo && this.fileSearch && await this.fileSearch.searchForFiles()); async (repo) => repo && this.fileSearch && await this.fileSearch.searchForFiles());
this.tagService.tags.subscribe(t => this.allTags = t); this.tagService.tags.subscribe(t => this.allTags = t);
} }
async ngOnInit() { async ngOnInit() {
this.fileSearch && await this.fileSearch.searchForFiles(); this.fileSearch && await this.fileSearch.searchForFiles();
if (this.tags.length === 0) { if (this.tags.length === 0) {
this.tags = this.tagsOfFiles; this.tags = this.tagsOfFiles;
}
} }
}
public async ngOnChanges(changes: SimpleChanges): Promise<void> { public async ngOnChanges(changes: SimpleChanges): Promise<void> {
if (changes["selectedFiles"]) { if (changes["selectedFiles"]) {
await this.showFileDetails(this.selectedFiles); await this.showFileDetails(this.selectedFiles);
this.showAllTagsFallback(); this.showAllTagsFallback();
}
} }
}
async loadTagsForDisplayedFiles() { async loadTagsForDisplayedFiles() {
this.tagsOfFiles = await this.tagService.getTagsForFiles( this.tagsOfFiles = await this.tagService.getTagsForFiles(
this.files.map(f => f.hash)); this.files.map(f => f.hash));
this.showAllTagsFallback(); this.showAllTagsFallback();
} }
async addSearchTag(tag: Tag) { async addSearchTag(tag: Tag) {
this.fileSearch.addSearchTag(tag.getNormalizedOutput()); this.fileSearch.addSearchTag(tag.getNormalizedOutput());
await this.fileSearch.searchForFiles(); await this.fileSearch.searchForFiles();
} }
async showFileDetails(files: File[]) { async showFileDetails(files: File[]) {
this.tagsOfSelection = await this.tagService.getTagsForFiles( this.tagsOfSelection = await this.tagService.getTagsForFiles(
files.map(f => f.hash)) 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()
this.tags = this.tagsOfSelection; .localeCompare(b.getNormalizedOutput()));
} this.tags = this.tagsOfSelection;
}
public async copyToClipboard(text: string) { public async copyToClipboard(text: string) {
await clipboard.writeText(text); 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);
if (filteredSelection.length === 0) { if (filteredSelection.length === 0) {
this.tags = []; this.tags = [];
this.showAllTagsFallback(); this.showAllTagsFallback();
} else if (filteredSelection.length < this.selectedFiles.length) { } else if (filteredSelection.length < this.selectedFiles.length) {
this.selectedFiles = filteredSelection; this.selectedFiles = filteredSelection;
await this.showFileDetails(this.selectedFiles); await this.showFileDetails(this.selectedFiles);
}
} }
}
private showAllTagsFallback() { private showAllTagsFallback() {
if (this.tags.length === 0) { if (this.tags.length === 0) {
this.tags = this.tagsOfFiles.sort( this.tags = this.tagsOfFiles.sort(
(a, b) => a.getNormalizedOutput() (a, b) => a.getNormalizedOutput()
.localeCompare(b.getNormalizedOutput())); .localeCompare(b.getNormalizedOutput()));
}
} }
}
} }

@ -1,12 +1,12 @@
<mat-drawer-container autosize> <mat-drawer-container autosize>
<mat-drawer disableClose mode="side" opened> <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" (searchStartEvent)="this.contentLoading = true"
[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 [blurBackground]="true" [busy]="contentLoading"> <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>
</mat-drawer-container> </mat-drawer-container>

@ -1,45 +1,45 @@
mat-selection-list { mat-selection-list {
height: 100%; height: 100%;
} }
mat-drawer { mat-drawer {
height: 100%; height: 100%;
width: 25%; width: 25%;
overflow: hidden; overflow: hidden;
} }
mat-drawer-content { mat-drawer-content {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
height: 100%; height: 100%;
} }
mat-drawer-container { mat-drawer-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
app-file-multiview { app-file-multiview {
padding: 0; padding: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.spinner-overlay { .spinner-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
z-index: 998; z-index: 998;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
mat-progress-spinner { mat-progress-spinner {
z-index: 999; z-index: 999;
margin: auto; margin: auto;
} }
} }

@ -1,25 +1,24 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FilesTabComponent} from './files-tab.component'; import {FilesTabComponent} from "./files-tab.component";
describe('SearchPageComponent', () => { describe("SearchPageComponent", () => {
let component: FilesTabComponent; let component: FilesTabComponent;
let fixture: ComponentFixture<FilesTabComponent>; let fixture: ComponentFixture<FilesTabComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FilesTabComponent] declarations: [FilesTabComponent]
}) }).compileComponents();
.compileComponents(); });
});
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FilesTabComponent); fixture = TestBed.createComponent(FilesTabComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,34 +1,34 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from "@angular/core";
import {File} from "../../../models/File"; import {File} from "../../../models/File";
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 {RepositoryService} from "../../../services/repository/repository.service"; import {RepositoryService} from "../../../services/repository/repository.service";
@Component({ @Component({
selector: 'app-files-tab', selector: "app-files-tab",
templateUrl: './files-tab.component.html', templateUrl: "./files-tab.component.html",
styleUrls: ['./files-tab.component.scss'] styleUrls: ["./files-tab.component.scss"]
}) })
export class FilesTabComponent implements OnInit { export class FilesTabComponent implements OnInit {
files: File[] = []; files: File[] = [];
contentLoading = false; contentLoading = false;
selectedFiles: File[] = []; selectedFiles: File[] = [];
constructor( constructor(
private errorBroker: ErrorBrokerService, private errorBroker: ErrorBrokerService,
private repoService: RepositoryService, private repoService: RepositoryService,
private fileService: FileService,) { private fileService: FileService,) {
} }
async ngOnInit() { async ngOnInit() {
this.fileService.displayedFiles.subscribe(async (files) => { this.fileService.displayedFiles.subscribe(async (files) => {
this.files = files; this.files = files;
}); });
} }
async onFileSelect(files: File[]) { async onFileSelect(files: File[]) {
this.selectedFiles = files; this.selectedFiles = files;
} }
} }

@ -1,35 +1,35 @@
<mat-form-field> <mat-form-field>
<mat-label>Selection Type</mat-label> <mat-label>Selection Type</mat-label>
<mat-select #selectionType value="files"> <mat-select #selectionType value="files">
<mat-option value="folders">Folders</mat-option> <mat-option value="folders">Folders</mat-option>
<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 (fileSelect)="this.setSelectedPaths($event)" [filters]="this.filters" <app-native-file-select (fileSelect)="this.setSelectedPaths($event)" [filters]="this.filters"
[mode]="selectionType.value"></app-native-file-select> [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" color="primary" mode="indeterminate"></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 (change)="this.importOptions.read_tags_from_txt = $event.checked" <mat-checkbox (change)="this.importOptions.read_tags_from_txt = $event.checked"
[checked]="this.importOptions.read_tags_from_txt">Import tags from [checked]="this.importOptions.read_tags_from_txt">Import tags from
adjacent .txt tag files adjacent .txt tag files
</mat-checkbox> </mat-checkbox>
<mat-checkbox (change)="this.importOptions.delete_after_import = $event.checked" <mat-checkbox (change)="this.importOptions.delete_after_import = $event.checked"
[checked]="this.importOptions.delete_after_import" color="warn"> [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 (click)="import()" [disabled]="importing || this.fileCount === 0" class="import-button" color="primary" <button (click)="import()" [disabled]="importing || this.fileCount === 0" class="import-button" color="primary"
mat-flat-button> mat-flat-button>
{{importing ? "Importing..." : "Import"}} {{importing ? "Importing..." : "Import"}}
</button> </button>
<mat-progress-bar *ngIf="importing" [value]="this.importingProgress" color="primary" <mat-progress-bar *ngIf="importing" [value]="this.importingProgress" color="primary"
mode="determinate"></mat-progress-bar> mode="determinate"></mat-progress-bar>

@ -1,25 +1,25 @@
app-native-file-select { app-native-file-select {
width: 100%; width: 100%;
} }
button { button {
width: 100%; width: 100%;
} }
mat-divider { mat-divider {
width: 100%; width: 100%;
height: 1px; height: 1px;
margin: 1em 0; margin: 1em 0;
} }
.filled-button { .filled-button {
background-color: #5c5c5c; background-color: #5c5c5c;
} }
.binary-import-options { .binary-import-options {
margin-top: 1em; margin-top: 1em;
mat-checkbox { mat-checkbox {
margin: 0.5em 0; margin: 0.5em 0;
} }
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FilesystemImportComponent} from './filesystem-import.component'; import {FilesystemImportComponent} from "./filesystem-import.component";
describe('FilesystemImportComponent', () => { describe("FilesystemImportComponent", () => {
let component: FilesystemImportComponent; let component: FilesystemImportComponent;
let fixture: ComponentFixture<FilesystemImportComponent>; let fixture: ComponentFixture<FilesystemImportComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FilesystemImportComponent] declarations: [FilesystemImportComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FilesystemImportComponent); fixture = TestBed.createComponent(FilesystemImportComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,4 +1,4 @@
import {Component, EventEmitter, Output} from '@angular/core'; import {Component, EventEmitter, Output} from "@angular/core";
import {FileOsMetadata} from "../../../../../models/FileOsMetadata"; import {FileOsMetadata} from "../../../../../models/FileOsMetadata";
import {ImportService} from "../../../../../services/import/import.service"; import {ImportService} from "../../../../../services/import/import.service";
import {ErrorBrokerService} from "../../../../../services/error-broker/error-broker.service"; import {ErrorBrokerService} from "../../../../../services/error-broker/error-broker.service";
@ -7,67 +7,70 @@ import {File} from "../../../../../models/File";
import {DialogFilter} from "@tauri-apps/api/dialog"; import {DialogFilter} from "@tauri-apps/api/dialog";
@Component({ @Component({
selector: 'app-filesystem-import', selector: "app-filesystem-import",
templateUrl: './filesystem-import.component.html', templateUrl: "./filesystem-import.component.html",
styleUrls: ['./filesystem-import.component.scss'] styleUrls: ["./filesystem-import.component.scss"]
}) })
export class FilesystemImportComponent { export class FilesystemImportComponent {
@Output() fileImported = new EventEmitter<File>(); @Output() fileImported = new EventEmitter<File>();
@Output() importFinished = new EventEmitter<void>(); @Output() importFinished = new EventEmitter<void>();
public fileCount: number = 0; public fileCount: number = 0;
public files: FileOsMetadata[] = []; public files: FileOsMetadata[] = [];
public importOptions = new AddFileOptions(); public importOptions = new AddFileOptions();
public filters: DialogFilter[] = [ public filters: DialogFilter[] = [
{name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "bmp", "gif"]}, {
{name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]}, name: "Images",
{name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"]}, extensions: ["png", "jpg", "jpeg", "webp", "bmp", "gif"]
{name: "Documents", extensions: ["pdf", "doc", "docx", "odf"]}, },
{name: "Text", extensions: ["txt", "md"]}, {name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]},
{name: "All", extensions: ["*"]} {name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"]},
] {name: "Documents", extensions: ["pdf", "doc", "docx", "odf"]},
{name: "Text", extensions: ["txt", "md"]},
{name: "All", extensions: ["*"]}
]
public resolving = false; public resolving = false;
public importing = false; public importing = false;
public importingProgress = 0; public importingProgress = 0;
constructor(private errorBroker: ErrorBrokerService, private importService: ImportService) { constructor(private errorBroker: ErrorBrokerService, private importService: ImportService) {
} }
public async setSelectedPaths(paths: string[]) { public async setSelectedPaths(paths: string[]) {
this.resolving = true; this.resolving = true;
try { try {
this.files = await this.importService.resolvePathsToFiles(paths); this.files = await this.importService.resolvePathsToFiles(paths);
this.fileCount = this.files.length; this.fileCount = this.files.length;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
this.errorBroker.showError(err); this.errorBroker.showError(err);
}
this.resolving = false;
} }
this.resolving = false;
}
public async import() { public async import() {
this.importing = true; this.importing = true;
this.importingProgress = 0; this.importingProgress = 0;
let count = 0; let count = 0;
for (const file of this.files) { for (const file of this.files) {
try { try {
const resultFile = await this.importService.addLocalFile(file, const resultFile = await this.importService.addLocalFile(file,
this.importOptions); this.importOptions);
this.fileImported.emit(resultFile); this.fileImported.emit(resultFile);
} catch (err) { } catch (err) {
console.log(err); console.log(err);
this.errorBroker.showError(err); this.errorBroker.showError(err);
} }
count++; count++;
this.importingProgress = (count / this.fileCount) * 100; this.importingProgress = (count / this.fileCount) * 100;
} }
this.importing = false; this.importing = false;
this.importFinished.emit(); this.importFinished.emit();
} }
} }

@ -1,21 +1,21 @@
<div class="import-tab-inner"> <div class="import-tab-inner">
<mat-tab-group headerPosition="below"> <mat-tab-group headerPosition="below">
<mat-tab label="Import"> <mat-tab label="Import">
<div class="import-sidebar-tab-inner" fxLayout="column"> <div class="import-sidebar-tab-inner" fxLayout="column">
<div class="import-type-select-wrapper" fxFlex="6em"> <div class="import-type-select-wrapper" fxFlex="6em">
<mat-form-field class="import-type-select"> <mat-form-field class="import-type-select">
<mat-label>Import Type</mat-label> <mat-label>Import Type</mat-label>
<mat-select value="filesystem"> <mat-select value="filesystem">
<mat-option value="filesystem">Filesystem</mat-option> <mat-option value="filesystem">Filesystem</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<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)" <app-filesystem-import (fileImported)="this.fileImported.emit($event)"
(importFinished)="importFinished.emit()"></app-filesystem-import> (importFinished)="importFinished.emit()"></app-filesystem-import>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>

@ -1,35 +1,35 @@
mat-tab-group, mat-tab { mat-tab-group, mat-tab {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.import-tab-inner { .import-tab-inner {
display: block; display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
.import-type-select-wrapper { .import-type-select-wrapper {
width: 100%; width: 100%;
.import-type-select { .import-type-select {
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%;
}
} }
}
} }
.import-sidebar-tab-inner { .import-sidebar-tab-inner {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.import-configuration { .import-configuration {
padding: 1em; padding: 1em;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ImportTabSidebarComponent} from './import-tab-sidebar.component'; import {ImportTabSidebarComponent} from "./import-tab-sidebar.component";
describe('ImportTabSidebarComponent', () => { describe("ImportTabSidebarComponent", () => {
let component: ImportTabSidebarComponent; let component: ImportTabSidebarComponent;
let fixture: ComponentFixture<ImportTabSidebarComponent>; let fixture: ComponentFixture<ImportTabSidebarComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ImportTabSidebarComponent] declarations: [ImportTabSidebarComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ImportTabSidebarComponent); fixture = TestBed.createComponent(ImportTabSidebarComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,16 +1,16 @@
import {Component, EventEmitter, Output} from '@angular/core'; import {Component, EventEmitter, Output} from "@angular/core";
import {File} from "../../../../models/File"; import {File} from "../../../../models/File";
@Component({ @Component({
selector: 'app-import-tab-sidebar', selector: "app-import-tab-sidebar",
templateUrl: './import-tab-sidebar.component.html', templateUrl: "./import-tab-sidebar.component.html",
styleUrls: ['./import-tab-sidebar.component.scss'] styleUrls: ["./import-tab-sidebar.component.scss"]
}) })
export class ImportTabSidebarComponent { 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,9 +1,9 @@
<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)" <app-import-tab-sidebar (fileImported)="this.addFileFromImport($event)"
(importFinished)="this.refreshFileView()"></app-import-tab-sidebar> (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>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>

@ -1,27 +1,27 @@
mat-drawer-container { mat-drawer-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;
} }
mat-drawer-content { mat-drawer-content {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
height: 100%; height: 100%;
} }
mat-drawer { mat-drawer {
height: 100%; height: 100%;
width: 25%; width: 25%;
} }
app-import-tab-sidebar, app-file-multiview { app-import-tab-sidebar, app-file-multiview {
height: 100%; height: 100%;
width: 100%; width: 100%;
margin: 0; margin: 0;
} }
app-file-multiview { app-file-multiview {
display: block; display: block;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ImportTabComponent} from './import-tab.component'; import {ImportTabComponent} from "./import-tab.component";
describe('ImportTabComponent', () => { describe("ImportTabComponent", () => {
let component: ImportTabComponent; let component: ImportTabComponent;
let fixture: ComponentFixture<ImportTabComponent>; let fixture: ComponentFixture<ImportTabComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ImportTabComponent] declarations: [ImportTabComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ImportTabComponent); fixture = TestBed.createComponent(ImportTabComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,35 +1,35 @@
import {Component} from '@angular/core'; import {Component} from "@angular/core";
import {File} from "../../../models/File"; import {File} from "../../../models/File";
@Component({ @Component({
selector: 'app-import-tab', selector: "app-import-tab",
templateUrl: './import-tab.component.html', templateUrl: "./import-tab.component.html",
styleUrls: ['./import-tab.component.scss'] styleUrls: ["./import-tab.component.scss"]
}) })
export class ImportTabComponent { export class ImportTabComponent {
public files: File[] = []; public files: File[] = [];
constructor() { constructor() {
} }
/** /**
* Adds an imported file to the list of imported files * Adds an imported file to the list of imported files
* @param {File} file * @param {File} file
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async addFileFromImport(file: File) { public async addFileFromImport(file: File) {
this.files.push(file); this.files.push(file);
if (this.files.length % 50 === 0) { // refresh every 50 pictures if (this.files.length % 50 === 0) { // refresh every 50 pictures
this.refreshFileView(); this.refreshFileView();
}
} }
}
/** /**
* Refreshes the file view * Refreshes the file view
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public refreshFileView() { public refreshFileView() {
this.files = [...this.files]; this.files = [...this.files];
} }
} }

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

@ -1,26 +1,26 @@
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.dialog-buttons { .dialog-buttons {
display: block; display: block;
width: 100%; width: 100%;
button { button {
margin-left: 1em; margin-left: 1em;
float: right float: right
} }
.check-connection-button { .check-connection-button {
justify-self: right; justify-self: right;
margin: 0; margin: 0;
float: left; float: left;
} }
} }
.button-folder-select { .button-folder-select {
position: absolute; position: absolute;
top: -10px; top: -10px;
right: 0; right: 0;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {AddRepositoryDialogComponent} from './add-repository-dialog.component'; import {AddRepositoryDialogComponent} from "./add-repository-dialog.component";
describe('AddRepositoryDialogComponent', () => { describe("AddRepositoryDialogComponent", () => {
let component: AddRepositoryDialogComponent; let component: AddRepositoryDialogComponent;
let fixture: ComponentFixture<AddRepositoryDialogComponent>; let fixture: ComponentFixture<AddRepositoryDialogComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [AddRepositoryDialogComponent] declarations: [AddRepositoryDialogComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AddRepositoryDialogComponent); fixture = TestBed.createComponent(AddRepositoryDialogComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,10 +1,10 @@
import {Component, Inject, OnInit} from '@angular/core'; import {Component, Inject, OnInit} from "@angular/core";
import { import {
AbstractControl, AbstractControl,
FormControl, FormControl,
FormGroup, FormGroup,
ValidationErrors, 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";
import {RepositoryService} from "../../../../services/repository/repository.service"; import {RepositoryService} from "../../../../services/repository/repository.service";
@ -13,129 +13,129 @@ import {dialog} from "@tauri-apps/api";
import {Repository} from "../../../../models/Repository"; import {Repository} from "../../../../models/Repository";
@Component({ @Component({
selector: 'app-add-repository-dialog', selector: "app-add-repository-dialog",
templateUrl: './add-repository-dialog.component.html', templateUrl: "./add-repository-dialog.component.html",
styleUrls: ['./add-repository-dialog.component.scss'] styleUrls: ["./add-repository-dialog.component.scss"]
}) })
export class AddRepositoryDialogComponent implements OnInit { export class AddRepositoryDialogComponent implements OnInit {
formGroup = new FormGroup({ formGroup = new FormGroup({
name: new FormControl("My Repository", [Validators.required]), name: new FormControl("My Repository", [Validators.required]),
repositoryType: new FormControl("local", [Validators.required]), repositoryType: new FormControl("local", [Validators.required]),
path: new FormControl("", [this.validatePath]), path: new FormControl("", [this.validatePath]),
address: new FormControl("", [this.validateAddress]) address: new FormControl("", [this.validateAddress])
}); });
repositories: Repository[] = []; repositories: Repository[] = [];
onlineStatus = "Unknown"; onlineStatus = "Unknown";
localRepoExists = false; localRepoExists = false;
constructor( constructor(
public repoService: RepositoryService, public repoService: RepositoryService,
public errorBroker: ErrorBrokerService, public errorBroker: ErrorBrokerService,
public dialogRef: MatDialogRef<AddRepositoryDialogComponent>, public dialogRef: MatDialogRef<AddRepositoryDialogComponent>,
@Inject(MAT_DIALOG_DATA) data: any) { @Inject(MAT_DIALOG_DATA) data: any) {
}
ngOnInit(): void {
this.repoService.repositories.subscribe(
repositories => this.repositories = repositories)
}
public async checkRepositoryStatus() {
this.onlineStatus = "Checking...";
const address = this.formGroup.value.address;
const running = await this.repoService.checkDaemonRunning(address);
console.log(running);
this.onlineStatus = running ? "Online" : "Offline";
}
public async checkLocalRepoExists() {
this.localRepoExists = await this.repoService.checkLocalRepositoryExists(
this.formGroup.value.path);
}
public async initLocalRepository() {
const path = this.formGroup.value.path;
await this.repoService.initRepository(path);
await this.checkLocalRepoExists();
}
public async addRepository() {
let {name, repositoryType, path, address} = this.formGroup.value;
path = repositoryType === "local" ? path : undefined;
address = repositoryType === "remote" ? address : undefined;
try {
await this.repoService.addRepository(name, path, address,
repositoryType === "local");
this.dialogRef.close();
} catch (err) {
this.errorBroker.showError(err);
} }
}
public closeDialog() { ngOnInit(): void {
this.dialogRef.close(); this.repoService.repositories.subscribe(
} repositories => this.repositories = repositories)
}
public async openFolderDialog() { public async checkRepositoryStatus() {
const path = await dialog.open({ this.onlineStatus = "Checking...";
directory: true, const address = this.formGroup.value.address;
multiple: false, const running = await this.repoService.checkDaemonRunning(address);
}); console.log(running);
this.formGroup.get("path")?.setValue(path); this.onlineStatus = running ? "Online" : "Offline";
await this.checkLocalRepoExists(); }
}
public async checkLocalRepoExists() {
public async onTypeChange(type: string) { this.localRepoExists = await this.repoService.checkLocalRepositoryExists(
setTimeout(() => { this.formGroup.value.path);
const path = this.formGroup.get("path"); }
const address = this.formGroup.get("address");
switch (type) { public async initLocalRepository() {
case "local": const path = this.formGroup.value.path;
address?.clearValidators(); await this.repoService.initRepository(path);
address?.setErrors(null); await this.checkLocalRepoExists();
path?.setValidators(this.validatePath); }
path?.setErrors(this.validatePath(path));
break; public async addRepository() {
case "remote": let {name, repositoryType, path, address} = this.formGroup.value;
path?.clearValidators(); path = repositoryType === "local" ? path : undefined;
path?.setErrors(null); address = repositoryType === "remote" ? address : undefined;
address?.setValidators(this.validateAddress); try {
address?.setErrors(this.validateAddress(address)); await this.repoService.addRepository(name, path, address,
break; repositoryType === "local");
} this.dialogRef.close();
}, 0); } catch (err) {
} this.errorBroker.showError(err);
}
validateName() { }
const control = this.formGroup.get("name");
const value = control?.value; public closeDialog() {
this.dialogRef.close();
if (this.repositories.find(r => r.name === value)) { }
control?.setErrors({nameAlreadyExists: value});
public async openFolderDialog() {
const path = await dialog.open({
directory: true,
multiple: false,
});
this.formGroup.get("path")?.setValue(path);
await this.checkLocalRepoExists();
} }
}
validatePath(control: AbstractControl): ValidationErrors | null { public async onTypeChange(type: string) {
const repositoryType = control.parent?.get( setTimeout(() => {
"repositoryType")?.value ?? "local"; const path = this.formGroup.get("path");
const address = this.formGroup.get("address");
switch (type) {
case "local":
address?.clearValidators();
address?.setErrors(null);
path?.setValidators(this.validatePath);
path?.setErrors(this.validatePath(path));
break;
case "remote":
path?.clearValidators();
path?.setErrors(null);
address?.setValidators(this.validateAddress);
address?.setErrors(this.validateAddress(address));
break;
}
}, 0);
}
validateName() {
const control = this.formGroup.get("name");
const value = control?.value;
if (repositoryType === "local") { if (this.repositories.find(r => r.name === value)) {
return control.value.length > 0 ? null : {valueRequired: control.value}; control?.setErrors({nameAlreadyExists: value});
}
} }
return null;
}
validateAddress(control: AbstractControl): ValidationErrors | null { validatePath(control: AbstractControl): ValidationErrors | null {
const repositoryType = control.parent?.get( const repositoryType = control.parent?.get(
"repositoryType")?.value ?? "remote"; "repositoryType")?.value ?? "local";
if (repositoryType === "remote") { if (repositoryType === "local") {
const match = /(\d+\.){3}\d+:\d+|\S+:\d+/.test(control.value) return control.value.length > 0 ? null : {valueRequired: control.value};
return match ? null : {invalidAddress: control.value}; }
return null;
} }
return null; validateAddress(control: AbstractControl): ValidationErrors | null {
} const repositoryType = control.parent?.get(
"repositoryType")?.value ?? "remote";
if (repositoryType === "remote") {
const match = /(\d+\.){3}\d+:\d+|\S+:\d+/.test(control.value)
return match ? null : {invalidAddress: control.value};
}
return null;
}
} }

@ -1,10 +1,10 @@
<div class="repo-page-content"> <div class="repo-page-content">
<div class="add-repo-tools"> <div class="add-repo-tools">
<button (click)="openAddRepositoryDialog()" color="primary" mat-flat-button>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">
<app-repository-card [repository]="repository"></app-repository-card> <app-repository-card [repository]="repository"></app-repository-card>
</div>
</div> </div>
</div>
</div> </div>

@ -2,31 +2,31 @@
.repository-container { .repository-container {
margin: 1em; margin: 1em;
} }
.repo-page-content { .repo-page-content {
margin: 0 10%; margin: 0 10%;
height: calc(100% - 2em); height: calc(100% - 2em);
} }
.add-repo-tools { .add-repo-tools {
height: 5em; height: 5em;
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
button { button {
margin: 1em; margin: 1em;
} }
} }
.repository-list { .repository-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto; overflow-y: auto;
height: calc(100% - 5em); height: calc(100% - 5em);
} }
app-repository-card { app-repository-card {
position: relative; position: relative;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {RepositoriesTabComponent} from './repositories-tab.component'; import {RepositoriesTabComponent} from "./repositories-tab.component";
describe('RepositoriesComponent', () => { describe("RepositoriesComponent", () => {
let component: RepositoriesTabComponent; let component: RepositoriesTabComponent;
let fixture: ComponentFixture<RepositoriesTabComponent>; let fixture: ComponentFixture<RepositoriesTabComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [RepositoriesTabComponent] declarations: [RepositoriesTabComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(RepositoriesTabComponent); fixture = TestBed.createComponent(RepositoriesTabComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,37 +1,37 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from "@angular/core";
import {Repository} from "../../../models/Repository"; import {Repository} from "../../../models/Repository";
import {RepositoryService} from "../../../services/repository/repository.service"; import {RepositoryService} from "../../../services/repository/repository.service";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {AddRepositoryDialogComponent} from "./add-repository-dialog/add-repository-dialog.component"; import {AddRepositoryDialogComponent} from "./add-repository-dialog/add-repository-dialog.component";
@Component({ @Component({
selector: 'app-repositories-tab', selector: "app-repositories-tab",
templateUrl: './repositories-tab.component.html', templateUrl: "./repositories-tab.component.html",
styleUrls: ['./repositories-tab.component.scss'] styleUrls: ["./repositories-tab.component.scss"]
}) })
export class RepositoriesTabComponent implements OnInit { export class RepositoriesTabComponent implements OnInit {
repositories: Repository[] = []; repositories: Repository[] = [];
constructor( constructor(
private repoService: RepositoryService, private repoService: RepositoryService,
public dialog: MatDialog public dialog: MatDialog
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
this.repoService.repositories.subscribe({ this.repoService.repositories.subscribe({
next: (repos) => { next: (repos) => {
this.repositories = repos; this.repositories = repos;
} }
}); });
} }
public openAddRepositoryDialog() { public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, { this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true, disableClose: true,
minWidth: "30%", minWidth: "30%",
minHeight: "30%", minHeight: "30%",
}); });
} }
} }

@ -1,37 +1,37 @@
<app-busy-indicator [darkenBackground]="true"> <app-busy-indicator [darkenBackground]="true">
<mat-card> <mat-card>
<mat-card-title>{{repository.name}}</mat-card-title> <mat-card-title>{{repository.name}}</mat-card-title>
<div [class]="'repository-status ' + this.getDaemonStatusClass()"> <div [class]="'repository-status ' + this.getDaemonStatusClass()">
<p>{{this.getDaemonStatusText()}}</p> <p>{{this.getDaemonStatusText()}}</p>
</div> </div>
<mat-card-content> <mat-card-content>
<p *ngIf="repository.local" class="repository-path">{{repository.path!}}</p> <p *ngIf="repository.local" class="repository-path">{{repository.path!}}</p>
<p *ngIf="!repository.local" class="repository-address">{{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 (click)="startDaemonAndSelectRepository()" *ngIf="!this.isSelectedRepository() && repository.local" <button (click)="startDaemonAndSelectRepository()" *ngIf="!this.isSelectedRepository() && repository.local"
color="primary" color="primary"
mat-flat-button>Open mat-flat-button>Open
</button> </button>
<button (click)="selectRepository()" *ngIf="!this.isSelectedRepository() && !repository.local" <button (click)="selectRepository()" *ngIf="!this.isSelectedRepository() && !repository.local"
[disabled]="!this.daemonRunning" [disabled]="!this.daemonRunning"
color="primary" mat-flat-button>Connect color="primary" mat-flat-button>Connect
</button> </button>
<button (click)="this.repoService.closeSelectedRepository()" <button (click)="this.repoService.closeSelectedRepository()"
*ngIf="this.isSelectedRepository() && repository.local" color="primary" *ngIf="this.isSelectedRepository() && repository.local" color="primary"
mat-flat-button>Close mat-flat-button>Close
</button> </button>
<button (click)="this.repoService.disconnectSelectedRepository()" <button (click)="this.repoService.disconnectSelectedRepository()"
*ngIf="this.isSelectedRepository() && !repository.local" color="primary" *ngIf="this.isSelectedRepository() && !repository.local" color="primary"
mat-flat-button>Disconnect mat-flat-button>Disconnect
</button> </button>
<button [mat-menu-trigger-for]="menu" class="menu-button" mat-button> <button [mat-menu-trigger-for]="menu" class="menu-button" mat-button>
<ng-icon name="mat-more-vert"></ng-icon> <ng-icon name="mat-more-vert"></ng-icon>
</button> </button>
<mat-menu #menu="matMenu"> <mat-menu #menu="matMenu">
<button (click)="removeRepository()" *ngIf="repository.local" mat-menu-item>Delete</button> <button (click)="removeRepository()" *ngIf="repository.local" mat-menu-item>Delete</button>
<button (click)="removeRepository()" *ngIf="!repository.local" mat-menu-item>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>
</app-busy-indicator> </app-busy-indicator>

@ -1,40 +1,40 @@
@import "../../../../../styles"; @import "../../../../../styles";
.repository-path { .repository-path {
color: lightgray; color: lightgray;
} }
.repository-address { .repository-address {
color: lightgray; color: lightgray;
font-family: "Fira Code Light", Monospaced, Consolas, monospace; font-family: "Fira Code Light", Monospaced, Consolas, monospace;
} }
.repository-status { .repository-status {
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
padding: 0.5em; padding: 0.5em;
width: 3em; width: 3em;
text-align: center; text-align: center;
border-bottom-left-radius: 1em; border-bottom-left-radius: 1em;
p { p {
margin: auto; margin: auto;
} }
} }
.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;
} }
button.menu-button { button.menu-button {
float: right; float: right;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {RepositoryCardComponent} from './repository-card.component'; import {RepositoryCardComponent} from "./repository-card.component";
describe('RepositoryCardComponent', () => { describe("RepositoryCardComponent", () => {
let component: RepositoryCardComponent; let component: RepositoryCardComponent;
let fixture: ComponentFixture<RepositoryCardComponent>; let fixture: ComponentFixture<RepositoryCardComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [RepositoryCardComponent] declarations: [RepositoryCardComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(RepositoryCardComponent); fixture = TestBed.createComponent(RepositoryCardComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,4 +1,4 @@
import {Component, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Component, Input, OnDestroy, OnInit, ViewChild} from "@angular/core";
import {Repository} from "../../../../models/Repository"; import {Repository} from "../../../../models/Repository";
import {RepositoryService} from "../../../../services/repository/repository.service"; import {RepositoryService} from "../../../../services/repository/repository.service";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
@ -8,106 +8,106 @@ import {ConfirmDialogComponent} from "../../../shared/confirm-dialog/confirm-dia
import {BusyIndicatorComponent} from "../../../shared/busy-indicator/busy-indicator.component"; import {BusyIndicatorComponent} from "../../../shared/busy-indicator/busy-indicator.component";
@Component({ @Component({
selector: 'app-repository-card', selector: "app-repository-card",
templateUrl: './repository-card.component.html', templateUrl: "./repository-card.component.html",
styleUrls: ['./repository-card.component.scss'] styleUrls: ["./repository-card.component.scss"]
}) })
export class RepositoryCardComponent implements OnInit, OnDestroy { export class RepositoryCardComponent implements OnInit, OnDestroy {
@Input() repository!: Repository; @Input() repository!: Repository;
@ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent; @ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent;
public daemonRunning: boolean = false; public daemonRunning: boolean = false;
statusRefreshInterval: number | undefined; statusRefreshInterval: number | undefined;
constructor( constructor(
public repoService: RepositoryService, public repoService: RepositoryService,
private router: Router, private router: Router,
private errorBroker: ErrorBrokerService, private errorBroker: ErrorBrokerService,
public dialog: MatDialog) { public dialog: MatDialog) {
} }
public async ngOnInit() { public async ngOnInit() {
if (!this.repository.local) { if (!this.repository.local) {
await this.checkRemoteRepositoryStatus(); await this.checkRemoteRepositoryStatus();
this.statusRefreshInterval = setInterval( this.statusRefreshInterval = setInterval(
async () => await this.checkRemoteRepositoryStatus(), 10000); async () => await this.checkRemoteRepositoryStatus(), 10000);
}
} }
}
public async ngOnDestroy(): Promise<void> { public async ngOnDestroy(): Promise<void> {
if (this.statusRefreshInterval != undefined) { if (this.statusRefreshInterval != undefined) {
clearInterval(this.statusRefreshInterval); clearInterval(this.statusRefreshInterval);
}
} }
}
public isSelectedRepository(): boolean { public isSelectedRepository(): boolean {
return this.repoService.selectedRepository.getValue()?.name === this.repository.name return this.repoService.selectedRepository.getValue()?.name === this.repository.name
} }
public async removeRepository() { public async removeRepository() {
await this.dialog.open(ConfirmDialogComponent, { await this.dialog.open(ConfirmDialogComponent, {
data: { data: {
title: "Remove repository", title: "Remove repository",
message: `Do you really want to remove the repository "${this.repository.name}"?`, message: `Do you really want to remove the repository "${this.repository.name}"?`,
confirmAction: "Remove", confirmAction: "Remove",
confirmColor: "warn" confirmColor: "warn"
} }
}).afterClosed().subscribe(async confirmation => { }).afterClosed().subscribe(async confirmation => {
if (confirmation === true) { if (confirmation === true) {
await this.repoService.removeRepository(this.repository.name); await this.repoService.removeRepository(this.repository.name);
} }
}); });
} }
public getDaemonStatusText(): string { public getDaemonStatusText(): string {
if (this.repository.local) { if (this.repository.local) {
return "Local"; return "Local";
} else if (this.daemonRunning) { } else if (this.daemonRunning) {
return "Online"; return "Online";
} else { } else {
return "Offline"; return "Offline";
}
} }
}
public getDaemonStatusClass(): string { public getDaemonStatusClass(): string {
if (this.repository.local) { if (this.repository.local) {
return "status-local"; return "status-local";
} else if (this.daemonRunning) { } else if (this.daemonRunning) {
return "status-online"; return "status-online";
} else { } else {
return "status-offline"; return "status-offline";
}
} }
}
public async startDaemonAndSelectRepository() { public async startDaemonAndSelectRepository() {
try { try {
if (!this.daemonRunning) { if (!this.daemonRunning) {
await this.repoService.startDaemon(this.repository.path!); await this.repoService.startDaemon(this.repository.path!);
this.daemonRunning = true; this.daemonRunning = true;
await new Promise((res, _) => { await new Promise((res, _) => {
setTimeout(res, 2000) // wait for the daemon to start setTimeout(res, 2000) // wait for the daemon to start
}); });
} }
await this.selectRepository(); await this.selectRepository();
} catch (err) { } catch (err) {
this.errorBroker.showError(err); this.errorBroker.showError(err);
}
} }
}
public async selectRepository() { public async selectRepository() {
this.busyIndicator.setBusy(true); this.busyIndicator.setBusy(true);
try { try {
await this.repoService.setRepository(this.repository); await this.repoService.setRepository(this.repository);
} catch (err) { } catch (err) {
this.errorBroker.showError(err); this.errorBroker.showError(err);
}
this.busyIndicator.setBusy(false);
} }
this.busyIndicator.setBusy(false);
}
async checkRemoteRepositoryStatus() { async checkRemoteRepositoryStatus() {
this.daemonRunning = await this.repoService.checkDaemonRunning( this.daemonRunning = await this.repoService.checkDaemonRunning(
this.repository.address!); this.repository.address!);
} }
} }

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

@ -1,31 +1,31 @@
.busy-indicator-overlay { .busy-indicator-overlay {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
z-index: 998; z-index: 998;
mat-progress-spinner { mat-progress-spinner {
z-index: 999; z-index: 999;
margin: auto; margin: auto;
} }
} }
.busy-indicator-overlay.blur { .busy-indicator-overlay.blur {
background-color: rgba(0, 0, 0, 0.5); background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
} }
.busy-indicator-overlay.darken { .busy-indicator-overlay.darken {
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
} }
::ng-deep app-busy-indicator { ::ng-deep app-busy-indicator {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
display: block; display: block;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {BusyIndicatorComponent} from './busy-indicator.component'; import {BusyIndicatorComponent} from "./busy-indicator.component";
describe('BusyIndicatorComponent', () => { describe("BusyIndicatorComponent", () => {
let component: BusyIndicatorComponent; let component: BusyIndicatorComponent;
let fixture: ComponentFixture<BusyIndicatorComponent>; let fixture: ComponentFixture<BusyIndicatorComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [BusyIndicatorComponent] declarations: [BusyIndicatorComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(BusyIndicatorComponent); fixture = TestBed.createComponent(BusyIndicatorComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,49 +1,49 @@
import {Component, Input} 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({
selector: 'app-busy-indicator', selector: "app-busy-indicator",
templateUrl: './busy-indicator.component.html', templateUrl: "./busy-indicator.component.html",
styleUrls: ['./busy-indicator.component.scss'] styleUrls: ["./busy-indicator.component.scss"]
}) })
export class BusyIndicatorComponent { export class BusyIndicatorComponent {
@Input() busy: boolean = false; @Input() busy: boolean = false;
@Input() blurBackground: boolean = false; @Input() blurBackground: boolean = false;
@Input() darkenBackground: boolean = false; @Input() darkenBackground: boolean = false;
@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;
} }
public wrapOperation<T>(operation: Function): T | undefined { public wrapOperation<T>(operation: Function): T | undefined {
this.setBusy(true) this.setBusy(true)
try { try {
const result = operation(); const result = operation();
this.setBusy(false); this.setBusy(false);
return result; return result;
} catch { } catch {
return undefined; return undefined;
} finally { } finally {
this.setBusy(false); this.setBusy(false);
}
} }
}
public async wrapAsyncOperation<T>(operation: Function): Promise<T | undefined> { public async wrapAsyncOperation<T>(operation: Function): Promise<T | undefined> {
this.setBusy(true) this.setBusy(true)
try { try {
const result = await operation(); const result = await operation();
this.setBusy(false); this.setBusy(false);
return result; return result;
} catch { } catch {
return undefined; return undefined;
} finally { } finally {
this.setBusy(false); this.setBusy(false);
}
} }
}
} }

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

@ -1,8 +1,8 @@
.confirm-dialog-actions { .confirm-dialog-actions {
display: block; display: block;
button { button {
float: right; float: right;
margin-left: 1em; margin-left: 1em;
} }
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ConfirmDialogComponent} from './confirm-dialog.component'; import {ConfirmDialogComponent} from "./confirm-dialog.component";
describe('ConfirmDialogComponent', () => { describe("ConfirmDialogComponent", () => {
let component: ConfirmDialogComponent; let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>; let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ConfirmDialogComponent] declarations: [ConfirmDialogComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ConfirmDialogComponent); fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,35 +1,35 @@
import {Component, Inject} 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";
@Component({ @Component({
selector: 'app-confirm-dialog', selector: "app-confirm-dialog",
templateUrl: './confirm-dialog.component.html', templateUrl: "./confirm-dialog.component.html",
styleUrls: ['./confirm-dialog.component.scss'] styleUrls: ["./confirm-dialog.component.scss"]
}) })
export class ConfirmDialogComponent { export class ConfirmDialogComponent {
title = ""; title = "";
message = ""; message = "";
confirmAction = ""; confirmAction = "";
confirmColor: ThemePalette = "primary"; confirmColor: ThemePalette = "primary";
denyAction = "Cancel"; denyAction = "Cancel";
denyColor: ThemePalette = "accent"; denyColor: ThemePalette = "accent";
constructor( constructor(
public dialogRef: MatDialogRef<ConfirmDialogComponent>, public dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject( @Inject(
MAT_DIALOG_DATA) data: { title: string, message: string, confirmAction: string, denyAction?: string, confirmColor?: ThemePalette, denyColor?: ThemePalette } MAT_DIALOG_DATA) data: { title: string, message: string, confirmAction: string, denyAction?: string, confirmColor?: ThemePalette, denyColor?: ThemePalette }
) { ) {
this.title = data.title; this.title = data.title;
this.message = data.message; this.message = data.message;
this.confirmAction = data.confirmAction; this.confirmAction = data.confirmAction;
this.denyAction = data.denyAction ?? this.denyAction; this.denyAction = data.denyAction ?? this.denyAction;
this.confirmColor = data.confirmColor ?? this.confirmColor; this.confirmColor = data.confirmColor ?? this.confirmColor;
this.denyColor = data.denyColor ?? this.denyColor; this.denyColor = data.denyColor ?? this.denyColor;
} }
public closeDialog(result: boolean) { public closeDialog(result: boolean) {
this.dialogRef.close(result); this.dialogRef.close(result);
} }
} }

@ -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" [src]="this.imageSrc" [class.scale-width]="scaleWidth && maximizeWidth" [src]="this.imageSrc"
[style]="{borderRadius: this.borderRadius}" alt=""> [style]="{borderRadius: this.borderRadius}" alt="">
</div> </div>

@ -1,21 +1,21 @@
.image-container { .image-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
} }
img { img {
margin: auto; margin: auto;
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
height: auto; height: auto;
width: auto; width: auto;
} }
img.scale-height { img.scale-height {
height: 100%; height: 100%;
} }
img.scale-width { img.scale-width {
width: 100%; width: 100%;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ContentAwareImageComponent} from './content-aware-image.component'; import {ContentAwareImageComponent} from "./content-aware-image.component";
describe('ContentAwareImageComponent', () => { describe("ContentAwareImageComponent", () => {
let component: ContentAwareImageComponent; let component: ContentAwareImageComponent;
let fixture: ComponentFixture<ContentAwareImageComponent>; let fixture: ComponentFixture<ContentAwareImageComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ContentAwareImageComponent] declarations: [ContentAwareImageComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ContentAwareImageComponent); fixture = TestBed.createComponent(ContentAwareImageComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,42 +1,42 @@
import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; import {Component, ElementRef, Input, OnInit, ViewChild} from "@angular/core";
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
@Component({ @Component({
selector: 'app-content-aware-image', selector: "app-content-aware-image",
templateUrl: './content-aware-image.component.html', templateUrl: "./content-aware-image.component.html",
styleUrls: ['./content-aware-image.component.scss'] styleUrls: ["./content-aware-image.component.scss"]
}) })
export class ContentAwareImageComponent implements OnInit { export class ContentAwareImageComponent implements OnInit {
@Input() imageSrc!: string | SafeResourceUrl; @Input() imageSrc!: string | SafeResourceUrl;
@Input() maximizeHeight: boolean = true; @Input() maximizeHeight: boolean = true;
@Input() maximizeWidth: boolean = true; @Input() maximizeWidth: boolean = true;
@Input() borderRadius: string | undefined; @Input() borderRadius: string | undefined;
@Input() decoding: "async" | "sync" | "auto" = "auto"; @Input() decoding: "async" | "sync" | "auto" = "auto";
@ViewChild("image") image: ElementRef<HTMLImageElement> | undefined; @ViewChild("image") image: ElementRef<HTMLImageElement> | undefined;
scaleWidth = false; scaleWidth = false;
constructor() { constructor() {
} }
public ngOnInit(): void { public ngOnInit(): void {
if (this.image) { if (this.image) {
this.image.nativeElement.decoding = this.decoding; this.image.nativeElement.decoding = this.decoding;
}
} }
}
/** /**
* Fits the image into the container * Fits the image into the container
* @param {HTMLImageElement} image * @param {HTMLImageElement} image
* @param {HTMLDivElement} imageContainer * @param {HTMLDivElement} imageContainer
*/ */
public adjustSize(image: HTMLImageElement, imageContainer: HTMLDivElement): void { public adjustSize(image: HTMLImageElement, imageContainer: HTMLDivElement): void {
const containerHeight = Math.abs(imageContainer.clientHeight); const containerHeight = Math.abs(imageContainer.clientHeight);
const containerWidth = Math.abs(imageContainer.clientWidth); const containerWidth = Math.abs(imageContainer.clientWidth);
const imageRelativeHeight = image.naturalHeight / containerHeight; const imageRelativeHeight = image.naturalHeight / containerHeight;
const imageRelativeWidth = image.naturalWidth / containerWidth; const imageRelativeWidth = image.naturalWidth / containerWidth;
this.scaleWidth = imageRelativeWidth > imageRelativeHeight; this.scaleWidth = imageRelativeWidth > imageRelativeHeight;
} }
} }

@ -1,4 +1,4 @@
<div [matMenuTriggerFor]="contextMenu" [style.left]="x" [style.top]="y" class="menu-anchor"></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,4 +1,4 @@
.menu-anchor { .menu-anchor {
visibility: hidden; visibility: hidden;
position: fixed; position: fixed;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ContextMenuComponent} from './context-menu.component'; import {ContextMenuComponent} from "./context-menu.component";
describe('ContextMenuComponent', () => { describe("ContextMenuComponent", () => {
let component: ContextMenuComponent; let component: ContextMenuComponent;
let fixture: ComponentFixture<ContextMenuComponent>; let fixture: ComponentFixture<ContextMenuComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ContextMenuComponent] declarations: [ContextMenuComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ContextMenuComponent); fixture = TestBed.createComponent(ContextMenuComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,26 +1,26 @@
import {Component, ViewChild,} from '@angular/core'; import {Component, ViewChild,} from "@angular/core";
import {MatMenuTrigger} from "@angular/material/menu"; import {MatMenuTrigger} from "@angular/material/menu";
@Component({ @Component({
selector: 'app-context-menu', selector: "app-context-menu",
templateUrl: './context-menu.component.html', templateUrl: "./context-menu.component.html",
styleUrls: ['./context-menu.component.scss'] styleUrls: ["./context-menu.component.scss"]
}) })
export class ContextMenuComponent { export class ContextMenuComponent {
public x: string = "0"; public x: string = "0";
public y: string = "0"; public y: string = "0";
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger @ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger
constructor() { constructor() {
} }
public onContextMenu(event: MouseEvent) { public onContextMenu(event: MouseEvent) {
event.preventDefault(); event.preventDefault();
this.x = event.clientX + "px"; this.x = event.clientX + "px";
this.y = event.clientY + "px"; this.y = event.clientY + "px";
this.menuTrigger.menu.focusFirstItem("mouse"); this.menuTrigger.menu.focusFirstItem("mouse");
this.menuTrigger.openMenu(); this.menuTrigger.openMenu();
} }
} }

@ -1,5 +1,5 @@
<app-context-menu #contextMenu> <app-context-menu #contextMenu>
<button (click)="this.copyFileHash()" mat-menu-item>Copy Hash</button> <button (click)="this.copyFileHash()" mat-menu-item>Copy Hash</button>
<button (click)="this.exportFile()" mat-menu-item>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,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FileContextMenuComponent} from './file-context-menu.component'; import {FileContextMenuComponent} from "./file-context-menu.component";
describe('FileContextMenuComponent', () => { describe("FileContextMenuComponent", () => {
let component: FileContextMenuComponent; let component: FileContextMenuComponent;
let fixture: ComponentFixture<FileContextMenuComponent>; let fixture: ComponentFixture<FileContextMenuComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FileContextMenuComponent] declarations: [FileContextMenuComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FileContextMenuComponent); fixture = TestBed.createComponent(FileContextMenuComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,4 +1,4 @@
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} from "@tauri-apps/api"; import {clipboard} from "@tauri-apps/api";
@ -7,37 +7,37 @@ import {ErrorBrokerService} from "../../../../services/error-broker/error-broker
import {FileHelper} from "../../../../services/file/file.helper"; import {FileHelper} from "../../../../services/file/file.helper";
@Component({ @Component({
selector: 'app-file-context-menu', selector: "app-file-context-menu",
templateUrl: './file-context-menu.component.html', templateUrl: "./file-context-menu.component.html",
styleUrls: ['./file-context-menu.component.scss'] styleUrls: ["./file-context-menu.component.scss"]
}) })
export class FileContextMenuComponent { export class FileContextMenuComponent {
public file!: File; public file!: File;
@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;
this.contextMenu.onContextMenu(event); this.contextMenu.onContextMenu(event);
} }
public async copyFileHash(): Promise<void> { public async copyFileHash(): Promise<void> {
await clipboard.writeText(this.file.hash); await clipboard.writeText(this.file.hash);
} }
public async exportFile(): Promise<void> { public async exportFile(): Promise<void> {
const path = await FileHelper.getFileDownloadLocation(this.file) const path = await FileHelper.getFileDownloadLocation(this.file)
if (path) { if (path) {
try { try {
await this.fileService.saveFile(this.file, path); await this.fileService.saveFile(this.file, path);
} catch (err) { } catch (err) {
this.errorBroker.showError(err); this.errorBroker.showError(err);
} }
}
} }
}
} }

@ -1,39 +1,39 @@
<div class="file-edit-inner" fxLayout="column"> <div class="file-edit-inner" fxLayout="column">
<div class="file-metadata" fxFlex="150px"> <div class="file-metadata" fxFlex="150px">
<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 (focusout)="this.changeFileName(fileNameInput.value)" matInput> <input #fileNameInput (focusout)="this.changeFileName(fileNameInput.value)" matInput>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tag-edit-list" fxFlex fxFlexAlign="start" fxFlexFill> <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 *cdkVirtualFor="let tag of tags" class="editable-tag"> <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 (click)="removeTag(tag)" class="tag-remove-button" mat-icon-button> <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>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>
</div> </div>
<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 (tagAdded)="this.editTag($event)" [allowInvalid]="true" <app-tag-input (tagAdded)="this.editTag($event)" [allowInvalid]="true"
[availableTags]="this.allTags"></app-tag-input> [availableTags]="this.allTags"></app-tag-input>
<button class="add-tag-button" mat-icon-button> <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>
<mat-form-field appearance="fill" class="form-field-mode">
<mat-label>Mode</mat-label>
<mat-select [(value)]="editMode">
<mat-option value="Toggle">Toggle</mat-option>
<mat-option value="Add">Add</mat-option>
<mat-option value="Remove">Remove</mat-option>
</mat-select>
</mat-form-field>
</div> </div>
<mat-form-field appearance="fill" class="form-field-mode">
<mat-label>Mode</mat-label>
<mat-select [(value)]="editMode">
<mat-option value="Toggle">Toggle</mat-option>
<mat-option value="Add">Add</mat-option>
<mat-option value="Remove">Remove</mat-option>
</mat-select>
</mat-form-field>
</div>
</div> </div>

@ -1,69 +1,69 @@
.file-metadata, .tag-input { .file-metadata, .tag-input {
width: 100%;
mat-form-field, app-tag-input {
width: 100%; width: 100%;
}
mat-form-field.form-field-mode { mat-form-field, app-tag-input {
width: 10em; width: 100%;
} }
mat-form-field.form-field-mode {
width: 10em;
}
} }
.file-edit-inner { .file-edit-inner {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: block; display: block;
} }
.tag-edit-list { .tag-edit-list {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: block; display: block;
overflow: hidden; overflow: hidden;
} }
cdk-virtual-scroll-viewport { cdk-virtual-scroll-viewport {
height: 100%; height: 100%;
width: 100%;
overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%; width: 100%;
} overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%;
}
} }
.editable-tag { .editable-tag {
height: 50px; height: 50px;
width: 100%; width: 100%;
display: flex; display: flex;
font-size: 1.2em; font-size: 1.2em;
transition-duration: 0.1s; transition-duration: 0.1s;
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
cursor: default; cursor: default;
app-tag-item { app-tag-item {
margin: auto auto auto 0.25em; margin: auto auto auto 0.25em;
} }
.tag-remove-button { .tag-remove-button {
margin-right: 1em; margin-right: 1em;
height: 50px; height: 50px;
width: 50px; width: 50px;
} }
} }
.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;
} }
} }
mat-divider { mat-divider {
width: 100%; width: 100%;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FileEditComponent} from './file-edit.component'; import {FileEditComponent} from "./file-edit.component";
describe('FileEditComponent', () => { describe("FileEditComponent", () => {
let component: FileEditComponent; let component: FileEditComponent;
let fixture: ComponentFixture<FileEditComponent>; let fixture: ComponentFixture<FileEditComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FileEditComponent] declarations: [FileEditComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FileEditComponent); fixture = TestBed.createComponent(FileEditComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,12 +1,12 @@
import { import {
Component, Component,
ElementRef, ElementRef,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
SimpleChanges, SimpleChanges,
ViewChild ViewChild
} from '@angular/core'; } from "@angular/core";
import {File} from "../../../models/File"; import {File} from "../../../models/File";
import {Tag} from "../../../models/Tag"; import {Tag} from "../../../models/Tag";
import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling"; import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
@ -14,138 +14,145 @@ import {TagService} from "../../../services/tag/tag.service";
import {FileService} from "../../../services/file/file.service"; import {FileService} from "../../../services/file/file.service";
@Component({ @Component({
selector: 'app-file-edit', selector: "app-file-edit",
templateUrl: './file-edit.component.html', templateUrl: "./file-edit.component.html",
styleUrls: ['./file-edit.component.scss'] styleUrls: ["./file-edit.component.scss"]
}) })
export class FileEditComponent implements OnInit, OnChanges { export class FileEditComponent implements OnInit, OnChanges {
@Input() files: File[] = []; @Input() files: File[] = [];
public tags: Tag[] = []; public tags: Tag[] = [];
public allTags: Tag[] = []; public allTags: 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[] } = {}; private fileTags: { [key: number]: Tag[] } = {};
constructor( constructor(
private tagService: TagService, private tagService: TagService,
private fileService: FileService, private fileService: FileService,
) { ) {
} }
async ngOnInit() { async ngOnInit() {
this.tagService.tags.subscribe(tags => this.allTags = tags); this.tagService.tags.subscribe(tags => this.allTags = tags);
await this.tagService.loadTags(); await this.tagService.loadTags();
await this.loadFileTags(); await this.loadFileTags();
this.resetFileNameInput(); this.resetFileNameInput();
} }
async ngOnChanges(changes: SimpleChanges) { async ngOnChanges(changes: SimpleChanges) {
if (changes["files"]) { if (changes["files"]) {
await this.loadFileTags() await this.loadFileTags()
this.resetFileNameInput(); this.resetFileNameInput();
}
} }
}
public async changeFileName(value: string) {
public async changeFileName(value: string) { const name = value.trim();
const name = value.trim();
if (name.length > 0) {
if (name.length > 0) { const file = this.files[0];
const file = this.files[0]; console.log("Updating name to", name);
console.log("Updating name to", name); const responseFile = await this.fileService.updateFileName(file,
const responseFile = await this.fileService.updateFileName(file, name); name);
console.log("Updated name"); console.log("Updated name");
file.name = responseFile.name; file.name = responseFile.name;
this.resetFileNameInput(); this.resetFileNameInput();
}
} }
}
public async editTag(tag: string): Promise<void> {
public async editTag(tag: string): Promise<void> { if (tag.length > 0) {
if (tag.length > 0) { let tagInstance = this.allTags.find(
let tagInstance = this.allTags.find(t => t.getNormalizedOutput() === tag); t => t.getNormalizedOutput() === tag);
if (!tagInstance) { if (!tagInstance) {
tagInstance = (await this.tagService.createTags([tag]))[0]; tagInstance = (await this.tagService.createTags([tag]))[0];
this.allTags.push(tagInstance); this.allTags.push(tagInstance);
} }
switch (this.editMode) { switch (this.editMode) {
case "Toggle": case "Toggle":
await this.toggleTag(tagInstance); await this.toggleTag(tagInstance);
break; break;
case "Add": case "Add":
await this.addTag(tagInstance); await this.addTag(tagInstance);
break; break;
case "Remove": case "Remove":
await this.removeTag(tagInstance); await this.removeTag(tagInstance);
break; break;
} }
}
} }
}
async toggleTag(tag: Tag) {
async toggleTag(tag: 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]; let addedTags = [];
let addedTags = []; let removedTags = [];
let removedTags = []; if (fileTags.findIndex(i => i.id === tag.id) < 0) {
if (fileTags.findIndex(i => i.id === tag.id) < 0) { addedTags.push(tag.id);
addedTags.push(tag.id); } else {
} else { removedTags.push(tag.id);
removedTags.push(tag.id); }
} this.fileTags[file.id] = await this.tagService.changeFileTags(
this.fileTags[file.id] = await this.tagService.changeFileTags(file.id, file.id,
addedTags, removedTags); addedTags, removedTags);
}
this.mapFileTagsToTagList();
const index = this.tags.indexOf(tag);
index >= 0 && this.tagScroll.scrollToIndex(index);
} }
this.mapFileTagsToTagList();
const index = this.tags.indexOf(tag); async addTag(tag: Tag) {
index >= 0 && this.tagScroll.scrollToIndex(index); for (const file of this.files) {
} if (this.fileTags[file.id].findIndex(t => t.id === tag.id) < 0) {
this.fileTags[file.id] = await this.tagService.changeFileTags(
async addTag(tag: Tag) { file.id,
for (const file of this.files) { [tag.id], []);
if (this.fileTags[file.id].findIndex(t => t.id === tag.id) < 0) { }
this.fileTags[file.id] = await this.tagService.changeFileTags(file.id, }
[tag.id], []); this.mapFileTagsToTagList();
} const index = this.tags.indexOf(tag);
index >= 0 && this.tagScroll.scrollToIndex(index);
} }
this.mapFileTagsToTagList();
const index = this.tags.indexOf(tag); public async removeTag(tag: Tag) {
index >= 0 && this.tagScroll.scrollToIndex(index); for (const file of this.files) {
} if (this.fileTags[file.id].findIndex(t => t.id === tag.id) >= 0) {
this.fileTags[file.id] = await this.tagService.changeFileTags(
public async removeTag(tag: Tag) { file.id,
for (const file of this.files) { [], [tag.id]);
if (this.fileTags[file.id].findIndex(t => t.id === tag.id) >= 0) { }
this.fileTags[file.id] = await this.tagService.changeFileTags(file.id, }
[], [tag.id]); this.mapFileTagsToTagList();
}
} }
this.mapFileTagsToTagList();
}
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( this.fileTags[file.id] = await this.tagService.getTagsForFiles(
[file.hash]); [file.hash]);
}
this.mapFileTagsToTagList();
} }
this.mapFileTagsToTagList();
}
private resetFileNameInput() { private resetFileNameInput() {
if (this.files.length === 1) { if (this.files.length === 1) {
this.fileNameInput.nativeElement.value = this.files[0].name ?? ""; this.fileNameInput.nativeElement.value = this.files[0].name ?? "";
}
} }
}
private mapFileTagsToTagList() {
private mapFileTagsToTagList() { 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(
tags.push( ...fileTags.filter(
...fileTags.filter(t => tags.findIndex(tag => tag.id === t.id) < 0)); 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 [src]="this.blobUrl" controls> <audio [src]="this.blobUrl" controls>
</audio> </audio>
</div> </div>

@ -1,9 +1,9 @@
.audio-container { .audio-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
audio { audio {
margin: auto; margin: auto;
} }
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {AudioViewerComponent} from './audio-viewer.component'; import {AudioViewerComponent} from "./audio-viewer.component";
describe('AudioViewerComponent', () => { describe("AudioViewerComponent", () => {
let component: AudioViewerComponent; let component: AudioViewerComponent;
let fixture: ComponentFixture<AudioViewerComponent>; let fixture: ComponentFixture<AudioViewerComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [AudioViewerComponent] declarations: [AudioViewerComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AudioViewerComponent); fixture = TestBed.createComponent(AudioViewerComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,15 +1,15 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from "@angular/core";
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
@Component({ @Component({
selector: 'app-audio-viewer', selector: "app-audio-viewer",
templateUrl: './audio-viewer.component.html', templateUrl: "./audio-viewer.component.html",
styleUrls: ['./audio-viewer.component.scss'] styleUrls: ["./audio-viewer.component.scss"]
}) })
export class AudioViewerComponent { export class AudioViewerComponent {
@Input() blobUrl!: SafeResourceUrl; @Input() blobUrl!: SafeResourceUrl;
constructor() { constructor() {
} }
} }

@ -3,7 +3,7 @@
<app-audio-viewer *ngIf="getContentType() === 'audio' && this.blobUrl" [blobUrl]="this.blobUrl!"></app-audio-viewer> <app-audio-viewer *ngIf="getContentType() === 'audio' && this.blobUrl" [blobUrl]="this.blobUrl!"></app-audio-viewer>
<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 (click)="this.downloadContent()" color="primary" mat-flat-button>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>

@ -1,21 +1,21 @@
app-image-viewer, app-video-viewer, app-audio-viewer { app-image-viewer, app-video-viewer, app-audio-viewer {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.download-prompt { .download-prompt {
width: 100%; width: 100%;
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,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ContentViewerComponent} from './content-viewer.component'; import {ContentViewerComponent} from "./content-viewer.component";
describe('ContentViewerComponent', () => { describe("ContentViewerComponent", () => {
let component: ContentViewerComponent; let component: ContentViewerComponent;
let fixture: ComponentFixture<ContentViewerComponent>; let fixture: ComponentFixture<ContentViewerComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ContentViewerComponent] declarations: [ContentViewerComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ContentViewerComponent); fixture = TestBed.createComponent(ContentViewerComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,12 +1,12 @@
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
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";
import {FileService} from "../../../../../services/file/file.service"; import {FileService} from "../../../../../services/file/file.service";
@ -17,95 +17,95 @@ import {BusyIndicatorComponent} from "../../../busy-indicator/busy-indicator.com
type ContentType = "image" | "video" | "audio" | "other"; type ContentType = "image" | "video" | "audio" | "other";
@Component({ @Component({
selector: 'app-content-viewer', selector: "app-content-viewer",
templateUrl: './content-viewer.component.html', templateUrl: "./content-viewer.component.html",
styleUrls: ['./content-viewer.component.scss'] styleUrls: ["./content-viewer.component.scss"]
}) })
export class ContentViewerComponent implements AfterViewInit, OnChanges, OnDestroy { export class ContentViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() file!: File; @Input() file!: File;
public contentUrl: SafeResourceUrl | undefined; public contentUrl: SafeResourceUrl | undefined;
public blobUrl: SafeResourceUrl | undefined; public blobUrl: SafeResourceUrl | undefined;
@ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent; @ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent;
constructor( constructor(
private errorBroker: ErrorBrokerService, private errorBroker: ErrorBrokerService,
private fileService: FileService private fileService: FileService
) { ) {
}
public async ngAfterViewInit() {
if (["audio", "video"].includes(this.getContentType())) {
await this.loadBlobUrl();
} else {
this.contentUrl = this.fileService.buildContentUrl(this.file);
} }
}
public async ngOnChanges(changes: SimpleChanges) { public async ngAfterViewInit() {
if (changes["file"]) { if (["audio", "video"].includes(this.getContentType())) {
if (["audio", "video"].includes( await this.loadBlobUrl();
this.getContentType()) && this.busyIndicator) { } else {
await this.loadBlobUrl(); this.contentUrl = this.fileService.buildContentUrl(this.file);
} else { }
this.contentUrl = this.fileService.buildContentUrl(this.file);
this.unloadBlobUrl();
}
} }
}
public ngOnDestroy(): void { public async ngOnChanges(changes: SimpleChanges) {
this.unloadBlobUrl(); if (changes["file"]) {
} if (["audio", "video"].includes(
this.getContentType()) && this.busyIndicator) {
await this.loadBlobUrl();
} else {
this.contentUrl = this.fileService.buildContentUrl(this.file);
this.unloadBlobUrl();
}
}
}
public getContentType(): ContentType { public ngOnDestroy(): void {
if (!this.file.mime_type) { this.unloadBlobUrl();
return "other";
} }
let mimeParts = this.file.mime_type.split("/");
const type = mimeParts.shift() ?? "other";
const subtype = mimeParts.shift() ?? "*";
switch (type) { public getContentType(): ContentType {
case "image": if (!this.file.mime_type) {
return "image"; return "other";
case "video": }
return "video"; let mimeParts = this.file.mime_type.split("/");
case "audio": const type = mimeParts.shift() ?? "other";
return "audio"; const subtype = mimeParts.shift() ?? "*";
default:
return "other"; switch (type) {
case "image":
return "image";
case "video":
return "video";
case "audio":
return "audio";
default:
return "other";
}
} }
}
public async downloadContent() { public async downloadContent() {
const path = await FileHelper.getFileDownloadLocation(this.file) const path = await FileHelper.getFileDownloadLocation(this.file)
if (path) { if (path) {
try { try {
await this.fileService.saveFile(this.file, path); await this.fileService.saveFile(this.file, path);
} catch (err) { } catch (err) {
this.errorBroker.showError(err); this.errorBroker.showError(err);
} }
}
} }
}
public async loadBlobUrl(): Promise<void> { public async loadBlobUrl(): Promise<void> {
await this.busyIndicator.wrapAsyncOperation(async () => { await this.busyIndicator.wrapAsyncOperation(async () => {
const startId = this.file.id; const startId = this.file.id;
this.unloadBlobUrl(); this.unloadBlobUrl();
const url = await this.fileService.readFile(this.file); const url = await this.fileService.readFile(this.file);
if (startId === this.file.id) { if (startId === this.file.id) {
this.blobUrl = url; this.blobUrl = url;
} }
}); });
} }
private unloadBlobUrl() { private unloadBlobUrl() {
if (this.blobUrl) { if (this.blobUrl) {
URL?.revokeObjectURL(this.blobUrl as string); URL?.revokeObjectURL(this.blobUrl as string);
this.blobUrl = undefined; this.blobUrl = undefined;
}
} }
}
} }

@ -1,16 +1,16 @@
<div (mouseenter)="this.mouseInImageView = true" (mouseleave)="this.mouseInImageView = false" <div (mouseenter)="this.mouseInImageView = true" (mouseleave)="this.mouseInImageView = false"
class="image-full-view-inner"> class="image-full-view-inner">
<div class="zoom-slider"> <div class="zoom-slider">
<mat-slider (input)="this.imageZoom=$event.value ?? 1" [value]="this.imageZoom" max="4" <mat-slider (input)="this.imageZoom=$event.value ?? 1" [value]="this.imageZoom" max="4"
min="0.5" step="0.1" vertical></mat-slider> min="0.5" step="0.1" vertical></mat-slider>
<button (click)="this.resetImage()" mat-icon-button> <button (click)="this.resetImage()" mat-icon-button>
<ng-icon name="mat-refresh"></ng-icon> <ng-icon name="mat-refresh"></ng-icon>
</button> </button>
</div> </div>
<div (cdkDragMoved)="this.onDragMoved($event)" <div (cdkDragMoved)="this.onDragMoved($event)"
[cdkDragFreeDragPosition]="this.imagePosition" cdkDrag class="image-drag-container"> [cdkDragFreeDragPosition]="this.imagePosition" cdkDrag class="image-drag-container">
<div [style]="{scale: this.imageZoom}" class="image-scale-container"> <div [style]="{scale: this.imageZoom}" class="image-scale-container">
<app-content-aware-image [imageSrc]="this.imageUrl" decoding="sync"></app-content-aware-image> <app-content-aware-image [imageSrc]="this.imageUrl" decoding="sync"></app-content-aware-image>
</div>
</div> </div>
</div>
</div> </div>

@ -1,35 +1,35 @@
.image-drag-container, .image-scale-container { .image-drag-container, .image-scale-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.image-scale-container { .image-scale-container {
display: block; display: block;
} }
.zoom-slider { .zoom-slider {
position: absolute; position: absolute;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
right: 1em; right: 1em;
bottom: 1em; bottom: 1em;
z-index: 10; z-index: 10;
opacity: 0.5; opacity: 0.5;
padding: 1em 0.5em; padding: 1em 0.5em;
transition-duration: 0.2s; transition-duration: 0.2s;
} }
.zoom-slider:hover { .zoom-slider:hover {
opacity: 1; opacity: 1;
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
border-radius: 1em; border-radius: 1em;
} }
.image-full-view-inner { .image-full-view-inner {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: block; display: block;
position: relative; position: relative;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {ImageViewerComponent} from './image-viewer.component'; import {ImageViewerComponent} from "./image-viewer.component";
describe('ImageViewerComponent', () => { describe("ImageViewerComponent", () => {
let component: ImageViewerComponent; let component: ImageViewerComponent;
let fixture: ComponentFixture<ImageViewerComponent>; let fixture: ComponentFixture<ImageViewerComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ImageViewerComponent] declarations: [ImageViewerComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ImageViewerComponent); fixture = TestBed.createComponent(ImageViewerComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,69 +1,69 @@
import { import {
Component, Component,
HostListener, HostListener,
Input, Input,
OnChanges, OnChanges,
SimpleChanges 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";
@Component({ @Component({
selector: 'app-image-viewer', selector: "app-image-viewer",
templateUrl: './image-viewer.component.html', templateUrl: "./image-viewer.component.html",
styleUrls: ['./image-viewer.component.scss'] styleUrls: ["./image-viewer.component.scss"]
}) })
export class ImageViewerComponent implements OnChanges { export class ImageViewerComponent implements OnChanges {
@Input() imageUrl!: SafeResourceUrl | string; @Input() imageUrl!: SafeResourceUrl | string;
public imageZoom = 1; public imageZoom = 1;
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"]) {
this.resetImage(); this.resetImage();
}
} }
}
public resetImage() { public resetImage() {
this.imageZoom = 1; this.imageZoom = 1;
this.imagePosition = {x: 0, y: 0}; this.imagePosition = {x: 0, y: 0};
} }
public onDragMoved($event: CdkDragMove<HTMLDivElement>): void { public onDragMoved($event: CdkDragMove<HTMLDivElement>): void {
this.imagePosition.x += $event.delta.x; this.imagePosition.x += $event.delta.x;
this.imagePosition.y += $event.delta.y; 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) {
case "Escape": case "Escape":
this.resetImage(); this.resetImage();
break; break;
}
} }
}
@HostListener("mousewheel", ["$event"]) @HostListener("mousewheel", ["$event"])
private handleScroll(event: any) { private handleScroll(event: any) {
if (this.mouseInImageView) { if (this.mouseInImageView) {
const delta = event.wheelDelta ?? event.detail; const delta = event.wheelDelta ?? event.detail;
if (delta > 0) { if (delta > 0) {
this.imageZoom += 0.2 this.imageZoom += 0.2
if (this.imageZoom > 4) { if (this.imageZoom > 4) {
this.imageZoom = 4; this.imageZoom = 4;
} }
} else if (delta < 0) { } else if (delta < 0) {
this.imageZoom -= 0.2 this.imageZoom -= 0.2
if (this.imageZoom < 0.5) { if (this.imageZoom < 0.5) {
this.imageZoom = 0.5; this.imageZoom = 0.5;
}
}
} }
}
} }
}
} }

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

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {VideoViewerComponent} from './video-viewer.component'; import {VideoViewerComponent} from "./video-viewer.component";
describe('VideoViewerComponent', () => { describe("VideoViewerComponent", () => {
let component: VideoViewerComponent; let component: VideoViewerComponent;
let fixture: ComponentFixture<VideoViewerComponent>; let fixture: ComponentFixture<VideoViewerComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [VideoViewerComponent] declarations: [VideoViewerComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(VideoViewerComponent); fixture = TestBed.createComponent(VideoViewerComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,11 +1,11 @@
import {Component, Input,} from '@angular/core'; import {Component, Input,} from "@angular/core";
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
@Component({ @Component({
selector: 'app-video-viewer', selector: "app-video-viewer",
templateUrl: './video-viewer.component.html', templateUrl: "./video-viewer.component.html",
styleUrls: ['./video-viewer.component.scss'] styleUrls: ["./video-viewer.component.scss"]
}) })
export class VideoViewerComponent { export class VideoViewerComponent {
@Input() blobUrl!: SafeResourceUrl; @Input() blobUrl!: SafeResourceUrl;
} }

@ -2,26 +2,26 @@
@use '../../../../../../../node_modules/@angular/material/index' as mat; @use '../../../../../../../node_modules/@angular/material/index' as mat;
@mixin color($theme) { @mixin color($theme) {
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);
$primary-palette: map.get($color-config, 'primary'); $primary-palette: map.get($color-config, 'primary');
div.image-wrapper.selected { div.image-wrapper.selected {
background-color: mat.get-color-from-palette($primary-palette, 'darker'); background-color: mat.get-color-from-palette($primary-palette, 'darker');
} }
} }
@mixin typography($theme) { @mixin typography($theme) {
} }
@mixin theme($theme) { @mixin theme($theme) {
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);
@if $color-config != null { @if $color-config != null {
@include color($theme); @include color($theme);
} }
$typography-config: mat.get-typography-config($theme); $typography-config: mat.get-typography-config($theme);
@if $typography-config != null { @if $typography-config != null {
@include typography($theme); @include typography($theme);
} }
} }

@ -1,4 +1,4 @@
<div (click)="fileSelectEvent.emit(this.file)" [class.selected]="this.file.selected" class="image-wrapper"> <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-file-thumbnail [file]="file.data"></app-file-thumbnail> <app-file-thumbnail [file]="file.data"></app-file-thumbnail>
</div> </div>

@ -1,18 +1,18 @@
app-file-thumbnail { app-file-thumbnail {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
display: block; display: block;
} }
.image-wrapper { .image-wrapper {
width: calc(100% - 20px); width: calc(100% - 20px);
height: calc(100% - 20px); height: calc(100% - 20px);
align-items: center; align-items: center;
text-align: center; text-align: center;
background-color: darken(dimgrey, 15); background-color: darken(dimgrey, 15);
padding: 10px; padding: 10px;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FileGalleryEntryComponent} from './file-gallery-entry.component'; import {FileGalleryEntryComponent} from "./file-gallery-entry.component";
describe('FileGalleryEntryComponent', () => { describe("FileGalleryEntryComponent", () => {
let component: FileGalleryEntryComponent; let component: FileGalleryEntryComponent;
let fixture: ComponentFixture<FileGalleryEntryComponent>; let fixture: ComponentFixture<FileGalleryEntryComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FileGalleryEntryComponent] declarations: [FileGalleryEntryComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FileGalleryEntryComponent); fixture = TestBed.createComponent(FileGalleryEntryComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,13 +1,13 @@
import { import {
Component, Component,
EventEmitter, EventEmitter,
Inject, Inject,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
Output, Output,
SimpleChanges 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";
import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser";
@ -15,40 +15,41 @@ import {ErrorBrokerService} from "../../../../../services/error-broker/error-bro
import {Selectable} from "../../../../../models/Selectable"; import {Selectable} from "../../../../../models/Selectable";
@Component({ @Component({
selector: 'app-file-gallery-entry', selector: "app-file-gallery-entry",
templateUrl: './file-gallery-entry.component.html', templateUrl: "./file-gallery-entry.component.html",
styleUrls: ['./file-gallery-entry.component.scss'] styleUrls: ["./file-gallery-entry.component.scss"]
}) })
export class FileGalleryEntryComponent implements OnInit, OnChanges { export class FileGalleryEntryComponent implements OnInit, OnChanges {
@Input() file!: Selectable<File>; @Input() file!: Selectable<File>;
@Output() fileSelectEvent = new EventEmitter<Selectable<File>>(); @Output() fileSelectEvent = new EventEmitter<Selectable<File>>();
contentUrl: SafeResourceUrl | undefined; contentUrl: SafeResourceUrl | undefined;
private cachedFile: File | undefined; private cachedFile: File | undefined;
private urlSetTimeout: number | undefined; private urlSetTimeout: number | undefined;
constructor(@Inject( constructor(@Inject(
DomSanitizer) private sanitizer: DomSanitizer, private fileService: FileService, private errorBroker: ErrorBrokerService) { DomSanitizer) private sanitizer: DomSanitizer, private fileService: FileService, private errorBroker: ErrorBrokerService) {
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
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
this.cachedFile = this.file.data; this.cachedFile = this.file.data;
this.setImageDelayed(); this.setImageDelayed();
}
} }
}
ngOnInit() { ngOnInit() {
this.cachedFile = this.file.data; this.cachedFile = this.file.data;
this.setImageDelayed(); this.setImageDelayed();
} }
private setImageDelayed() { private setImageDelayed() {
this.contentUrl = undefined; this.contentUrl = undefined;
clearTimeout(this.urlSetTimeout); clearTimeout(this.urlSetTimeout);
this.urlSetTimeout = setTimeout( this.urlSetTimeout = setTimeout(
() => this.contentUrl = this.fileService.buildThumbnailUrl(this.file.data, () => this.contentUrl = this.fileService.buildThumbnailUrl(
250, 250), 200); this.file.data,
} 250, 250), 200);
}
} }

@ -1,21 +1,23 @@
<div class="gallery-container" fxLayout="column"> <div class="gallery-container" fxLayout="column">
<button (click)="this.closeEvent.emit(this)" class="close-button" mat-icon-button> <button (click)="this.closeEvent.emit(this)" class="close-button" mat-icon-button>
<ng-icon name="mat-close"></ng-icon> <ng-icon name="mat-close"></ng-icon>
</button> </button>
<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"
fxFlex="80%"> class="file-full-view"
<app-content-viewer fxFlex="80%">
(contextmenu)="this.selectedFile && fileContextMenu.onContextMenu($event, this.selectedFile!.data)" <app-content-viewer
[file]="this.selectedFile!.data"></app-content-viewer> (contextmenu)="this.selectedFile && fileContextMenu.onContextMenu($event, this.selectedFile!.data)"
</div> [file]="this.selectedFile!.data"></app-content-viewer>
<mat-divider fxFlex></mat-divider> </div>
<div class="file-scroll-view" fxFlex="20%"> <mat-divider fxFlex></mat-divider>
<cdk-virtual-scroll-viewport #virtualScroll class="file-scroll-viewport" itemSize="260" maxBufferPx="3000" <div class="file-scroll-view" fxFlex="20%">
minBufferPx="1000" orientation="horizontal"> <cdk-virtual-scroll-viewport #virtualScroll class="file-scroll-viewport" itemSize="260" maxBufferPx="3000"
<div *cdkVirtualFor="let entry of entries" class="file-item"> minBufferPx="1000" orientation="horizontal">
<app-file-gallery-entry (fileSelectEvent)="onEntrySelect($event)" [file]="entry"></app-file-gallery-entry> <div *cdkVirtualFor="let entry of entries" class="file-item">
</div> <app-file-gallery-entry (fileSelectEvent)="onEntrySelect($event)"
</cdk-virtual-scroll-viewport> [file]="entry"></app-file-gallery-entry>
</div> </div>
</cdk-virtual-scroll-viewport>
</div>
</div> </div>
<app-file-context-menu #fileContextMenu></app-file-context-menu> <app-file-context-menu #fileContextMenu></app-file-context-menu>

@ -1,62 +1,62 @@
.file-scroll-viewport { .file-scroll-viewport {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
::ng-deep .file-scroll-viewport > .cdk-virtual-scroll-content-wrapper { ::ng-deep .file-scroll-viewport > .cdk-virtual-scroll-content-wrapper {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100%; height: 100%;
} }
.gallery-container { .gallery-container {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative; position: relative;
user-select: none; user-select: none;
} }
app-file-gallery-entry, .file-item { app-file-gallery-entry, .file-item {
width: 250px; width: 250px;
height: calc(100% - 10px); height: calc(100% - 10px);
padding: 5px; padding: 5px;
} }
app-file-gallery-entry { app-file-gallery-entry {
display: block; display: block;
} }
.file-full-view { .file-full-view {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
app-content-viewer { app-content-viewer {
height: 100%; height: 100%;
width: 100%; width: 100%;
display: block; display: block;
} }
.close-button { .close-button {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
width: 3em; width: 3em;
height: 3em; height: 3em;
z-index: 999; z-index: 999;
} }
.url-loading-backdrop { .url-loading-backdrop {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
mat-progress-spinner { mat-progress-spinner {
margin: auto; margin: auto;
} }
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FileGalleryComponent} from './file-gallery.component'; import {FileGalleryComponent} from "./file-gallery.component";
describe('FileGalleryComponent', () => { describe("FileGalleryComponent", () => {
let component: FileGalleryComponent; let component: FileGalleryComponent;
let fixture: ComponentFixture<FileGalleryComponent>; let fixture: ComponentFixture<FileGalleryComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FileGalleryComponent] declarations: [FileGalleryComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FileGalleryComponent); fixture = TestBed.createComponent(FileGalleryComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,14 +1,14 @@
import { import {
Component, Component,
EventEmitter, EventEmitter,
HostListener, HostListener,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
Output, Output,
SimpleChanges, SimpleChanges,
ViewChild 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";
@ -17,157 +17,162 @@ import {CdkVirtualScrollViewport} from "@angular/cdk/scrolling";
import {TabService} from "../../../../services/tab/tab.service"; import {TabService} from "../../../../services/tab/tab.service";
@Component({ @Component({
selector: 'app-file-gallery', selector: "app-file-gallery",
templateUrl: './file-gallery.component.html', templateUrl: "./file-gallery.component.html",
styleUrls: ['./file-gallery.component.scss'] styleUrls: ["./file-gallery.component.scss"]
}) })
export class FileGalleryComponent implements OnChanges, OnInit { export class FileGalleryComponent implements OnChanges, OnInit {
@Input() files: File[] = []; @Input() files: File[] = [];
@Input() preselectedFile: File | undefined; @Input() preselectedFile: File | undefined;
@Output() fileSelectEvent = new EventEmitter<File | undefined>(); @Output() fileSelectEvent = new EventEmitter<File | undefined>();
@Output() fileDblClickEvent = new EventEmitter<File>(); @Output() fileDblClickEvent = new EventEmitter<File>();
@Output() closeEvent = new EventEmitter<FileGalleryComponent>(); @Output() closeEvent = new EventEmitter<FileGalleryComponent>();
entries: Selectable<File>[] = []; entries: Selectable<File>[] = [];
@ViewChild("virtualScroll") virtualScroll!: CdkVirtualScrollViewport; @ViewChild("virtualScroll") virtualScroll!: CdkVirtualScrollViewport;
public selectedFile: Selectable<File> | undefined; public selectedFile: Selectable<File> | undefined;
public fileContentUrl: SafeResourceUrl | undefined; public fileContentUrl: SafeResourceUrl | undefined;
private scrollTimeout: number | undefined; private scrollTimeout: number | undefined;
constructor(private tabService: TabService, private fileService: FileService) { constructor(private tabService: TabService, private fileService: FileService) {
tabService.selectedTab.subscribe(() => this.adjustElementSizes()); tabService.selectedTab.subscribe(() => this.adjustElementSizes());
}
/**
* Called when a new entry is selected
* @param {Selectable<File>} entry
* @returns {Promise<void>}
*/
async onEntrySelect(entry: Selectable<File>) {
if (entry) {
this.selectedFile?.unselect();
entry.select();
this.selectedFile = entry;
await this.loadSelectedFile();
if (this.virtualScroll) {
clearTimeout(this.scrollTimeout);
this.scrollTimeout = setTimeout(() => this.scrollToSelection(), 0); // we need to make sure the viewport has rendered
}
this.fileSelectEvent.emit(this.selectedFile.data);
} }
}
/**
/** * Called when a new entry is selected
* Loads the content url of the selected file * @param {Selectable<File>} entry
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async loadSelectedFile() { async onEntrySelect(entry: Selectable<File>) {
if (this.selectedFile) { if (entry) {
this.fileContentUrl = this.fileService.buildContentUrl( this.selectedFile?.unselect();
this.selectedFile.data) entry.select();
this.selectedFile = entry;
await this.loadSelectedFile();
if (this.virtualScroll) {
clearTimeout(this.scrollTimeout);
this.scrollTimeout = setTimeout(() => this.scrollToSelection(),
0); // we need to make sure the viewport has rendered
}
this.fileSelectEvent.emit(this.selectedFile.data);
}
} }
}
async ngOnInit(): Promise<void> { /**
if (!this.selectedFile || this.files.indexOf(this.selectedFile.data) < 0) { * Loads the content url of the selected file
await this.onEntrySelect(this.getPreselectedEntry() ?? this.entries[0]) * @returns {Promise<void>}
*/
async loadSelectedFile() {
if (this.selectedFile) {
this.fileContentUrl = this.fileService.buildContentUrl(
this.selectedFile.data)
}
} }
}
async ngOnInit(): Promise<void> {
public async ngOnChanges(changes: SimpleChanges): Promise<void> { if (!this.selectedFile || this.files.indexOf(
if (changes["files"]) { this.selectedFile.data) < 0) {
this.entries = this.files.map( await this.onEntrySelect(
f => new Selectable(f, f.hash == this.selectedFile?.data.hash)); this.getPreselectedEntry() ?? this.entries[0])
const selectedIndex = this.files.findIndex( }
f => f.hash === this.selectedFile?.data.hash);
if (!this.selectedFile || selectedIndex < 0) {
await this.onEntrySelect(this.getPreselectedEntry() ?? this.entries[0])
} else {
await this.onEntrySelect(this.entries[selectedIndex])
}
} }
}
public async ngOnChanges(changes: SimpleChanges): Promise<void> {
/** if (changes["files"]) {
* Selects the previous item in the gallery this.entries = this.files.map(
* @returns {Promise<void>} f => new Selectable(f, f.hash == this.selectedFile?.data.hash));
*/ const selectedIndex = this.files.findIndex(
public async nextItem() { f => f.hash === this.selectedFile?.data.hash);
if (this.selectedFile) {
let index = this.entries.indexOf(this.selectedFile) + 1; if (!this.selectedFile || selectedIndex < 0) {
if (index == this.entries.length) { await this.onEntrySelect(
index--; // restrict to elements this.getPreselectedEntry() ?? this.entries[0])
} } else {
await this.onEntrySelect(this.entries[index]); await this.onEntrySelect(this.entries[selectedIndex])
} else { }
await this.onEntrySelect(this.entries[0]) }
} }
}
/**
/** * Selects the previous item in the gallery
* Selects the next item in the gallery * @returns {Promise<void>}
* @returns {Promise<void>} */
*/ public async nextItem() {
public async previousItem() { if (this.selectedFile) {
if (this.selectedFile) { let index = this.entries.indexOf(this.selectedFile) + 1;
let index = this.entries.indexOf(this.selectedFile) - 1; if (index == this.entries.length) {
if (index < 0) { index--; // restrict to elements
index++; // restrict to elements }
} await this.onEntrySelect(this.entries[index]);
await this.onEntrySelect(this.entries[index]); } else {
} else { await this.onEntrySelect(this.entries[0])
await this.onEntrySelect(this.entries[0]) }
}
/**
* Selects the next item in the gallery
* @returns {Promise<void>}
*/
public async previousItem() {
if (this.selectedFile) {
let index = this.entries.indexOf(this.selectedFile) - 1;
if (index < 0) {
index++; // restrict to elements
}
await this.onEntrySelect(this.entries[index]);
} else {
await this.onEntrySelect(this.entries[0])
}
} }
}
public adjustElementSizes(): void { public adjustElementSizes(): void {
if (this.virtualScroll) { if (this.virtualScroll) {
this.virtualScroll.checkViewportSize(); this.virtualScroll.checkViewportSize();
this.scrollToSelection(); this.scrollToSelection();
}
} }
}
private scrollToSelection(): void {
private scrollToSelection(): void { if (this.selectedFile) {
if (this.selectedFile) { const selectedIndex = this.entries.indexOf(this.selectedFile);
const selectedIndex = this.entries.indexOf(this.selectedFile); 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(
this.virtualScroll.scrollToIndex( Math.max(selectedIndex - indexAdjustment, 0), "smooth");
Math.max(selectedIndex - indexAdjustment, 0), "smooth");
if (selectedIndex > indexAdjustment) {
if (selectedIndex > indexAdjustment) { this.virtualScroll.scrollToOffset(
this.virtualScroll.scrollToOffset( this.virtualScroll.measureScrollOffset("left") + 130,
this.virtualScroll.measureScrollOffset("left") + 130, "smooth"); "smooth");
} }
}
} }
}
@HostListener("window:keydown", ["$event"])
@HostListener("window:keydown", ["$event"]) private async handleKeydownEvent(event: KeyboardEvent) {
private async handleKeydownEvent(event: KeyboardEvent) { switch (event.key) {
switch (event.key) { case "ArrowRight":
case "ArrowRight": await this.nextItem();
await this.nextItem(); break;
break; case "ArrowLeft":
case "ArrowLeft": await this.previousItem();
await this.previousItem(); break;
break; }
} }
}
private getPreselectedEntry(): Selectable<File> | undefined {
private getPreselectedEntry(): Selectable<File> | undefined { if (this.preselectedFile) {
if (this.preselectedFile) { const entry = this.entries.find(
const entry = this.entries.find( e => e.data.id === this.preselectedFile?.id);
e => e.data.id === this.preselectedFile?.id); if (entry) {
if (entry) { return entry;
return entry; }
} }
return undefined;
} }
return undefined;
}
} }

@ -1,6 +1,6 @@
import {File} from "../../../../../models/File"; import {File} from "../../../../../models/File";
export type GridEntry = { export type GridEntry = {
file: File, file: File,
selected: boolean, selected: boolean,
} }

@ -2,25 +2,25 @@
@use '../../../../../../../node_modules/@angular/material/index' as mat; @use '../../../../../../../node_modules/@angular/material/index' as mat;
@mixin color($theme) { @mixin color($theme) {
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);
$primary-palette: map.get($color-config, 'primary'); $primary-palette: map.get($color-config, 'primary');
mat-card.selected { mat-card.selected {
background-color: mat.get-color-from-palette($primary-palette, 'darker'); background-color: mat.get-color-from-palette($primary-palette, 'darker');
} }
} }
@mixin typography($theme) { @mixin typography($theme) {
} }
@mixin theme($theme) { @mixin theme($theme) {
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);
@if $color-config != null { @if $color-config != null {
@include color($theme); @include color($theme);
} }
$typography-config: mat.get-typography-config($theme); $typography-config: mat.get-typography-config($theme);
@if $typography-config != null { @if $typography-config != null {
@include typography($theme); @include typography($theme);
} }
} }

@ -1,6 +1,6 @@
<mat-card #card (click)="clickEvent.emit(this)" (dblclick)="dblClickEvent.emit(this)" <mat-card #card (click)="clickEvent.emit(this)" (dblclick)="dblClickEvent.emit(this)"
[ngClass]="{'selected': gridEntry.selected}"> [ngClass]="{'selected': gridEntry.selected}">
<mat-card-content *ngIf="contentUrl !== undefined"> <mat-card-content *ngIf="contentUrl !== undefined">
<app-file-thumbnail [file]="this.gridEntry.file" class=".entry-image"></app-file-thumbnail> <app-file-thumbnail [file]="this.gridEntry.file" class=".entry-image"></app-file-thumbnail>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

@ -1,16 +1,16 @@
mat-card { mat-card {
height: calc(100% - 32px); height: calc(100% - 32px);
width: calc(100% - 32px); width: calc(100% - 32px);
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
} }
mat-card-content { mat-card-content {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.entry-image { .entry-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }

@ -1,25 +1,25 @@
import {ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from "@angular/core/testing";
import {FileGridEntryComponent} from './file-grid-entry.component'; import {FileGridEntryComponent} from "./file-grid-entry.component";
describe('FileGridEntryComponent', () => { describe("FileGridEntryComponent", () => {
let component: FileGridEntryComponent; let component: FileGridEntryComponent;
let fixture: ComponentFixture<FileGridEntryComponent>; let fixture: ComponentFixture<FileGridEntryComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FileGridEntryComponent] declarations: [FileGridEntryComponent]
}) })
.compileComponents(); .compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FileGridEntryComponent); fixture = TestBed.createComponent(FileGridEntryComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

@ -1,57 +1,56 @@
import { import {
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
Output, Output,
SimpleChanges, SimpleChanges,
ViewChild 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 {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
import {GridEntry} from "./GridEntry"; import {GridEntry} from "./GridEntry";
@Component({ @Component({
selector: 'app-file-grid-entry', selector: "app-file-grid-entry",
templateUrl: './file-grid-entry.component.html', templateUrl: "./file-grid-entry.component.html",
styleUrls: ['./file-grid-entry.component.scss'] styleUrls: ["./file-grid-entry.component.scss"]
}) })
export class FileGridEntryComponent implements OnInit, OnChanges { export class FileGridEntryComponent implements OnInit, OnChanges {
@ViewChild("card") card!: ElementRef; @ViewChild("card") card!: ElementRef;
@Input() public gridEntry!: GridEntry; @Input() public gridEntry!: GridEntry;
@Output() clickEvent = new EventEmitter<FileGridEntryComponent>(); @Output() clickEvent = new EventEmitter<FileGridEntryComponent>();
@Output() dblClickEvent = new EventEmitter<FileGridEntryComponent>(); @Output() dblClickEvent = new EventEmitter<FileGridEntryComponent>();
contentUrl: SafeResourceUrl | undefined; contentUrl: SafeResourceUrl | undefined;
private cachedFile: File | undefined; private cachedFile: File | undefined;
private urlSetTimeout: number | undefined; private urlSetTimeout: number | undefined;
constructor(private fileService: FileService) { constructor(private fileService: FileService) {
} }
async ngOnInit() { async ngOnInit() {
this.cachedFile = this.gridEntry.file; this.cachedFile = this.gridEntry.file;
this.setImageDelayed(); this.setImageDelayed();
} }
async ngOnChanges(changes: SimpleChanges) { async ngOnChanges(changes: SimpleChanges) {
if (changes["file"] && (!this.cachedFile || this.gridEntry.file.hash !== this.cachedFile.hash)) { if (changes["file"] && (!this.cachedFile || this.gridEntry.file.hash !== this.cachedFile.hash)) {
this.cachedFile = this.gridEntry.file; this.cachedFile = this.gridEntry.file;
this.setImageDelayed(); this.setImageDelayed();
}
} }
}
private setImageDelayed() { private setImageDelayed() {
this.contentUrl = undefined; this.contentUrl = undefined;
clearTimeout(this.urlSetTimeout); clearTimeout(this.urlSetTimeout);
this.urlSetTimeout = setTimeout( this.urlSetTimeout = setTimeout(
() => this.contentUrl = this.fileService.buildThumbnailUrl( () => this.contentUrl = this.fileService.buildThumbnailUrl(
this.gridEntry.file, this.gridEntry.file,
250, 250), 200); 250, 250), 200);
} }
} }

@ -1,18 +1,19 @@
<div class="file-gallery-inner"> <div class="file-gallery-inner">
<cdk-virtual-scroll-viewport #virtualScrollGrid class="file-scroll" itemSize="250" maxBufferPx="2000" <cdk-virtual-scroll-viewport #virtualScrollGrid class="file-scroll" itemSize="250" maxBufferPx="2000"
minBufferPx="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 <app-file-grid-entry
(clickEvent)="setSelectedFile($event.gridEntry)" (contextmenu)="fileContextMenu.onContextMenu($event, gridEntry.file)" (clickEvent)="setSelectedFile($event.gridEntry)"
(dblClickEvent)="fileOpenEvent.emit($event.gridEntry.file)" (contextmenu)="fileContextMenu.onContextMenu($event, gridEntry.file)"
*ngFor="let gridEntry of rowEntry" (dblClickEvent)="fileOpenEvent.emit($event.gridEntry.file)"
[gridEntry]="gridEntry"></app-file-grid-entry> *ngFor="let gridEntry of rowEntry"
</div> [gridEntry]="gridEntry"></app-file-grid-entry>
</div> </div>
</cdk-virtual-scroll-viewport> </div>
</cdk-virtual-scroll-viewport>
</div> </div>
<app-file-context-menu #fileContextMenu> <app-file-context-menu #fileContextMenu>
<button (click)="this.regenerateThumbnail(fileContextMenu.file)" mat-menu-item>Regenerate thumbnail</button> <button (click)="this.regenerateThumbnail(fileContextMenu.file)" mat-menu-item>Regenerate thumbnail</button>
</app-file-context-menu> </app-file-context-menu>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save