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
indent_style = space
indent_size = 2
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
quote_type = double
[*.md]
max_line_length = off

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

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

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

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

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

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

@ -1,20 +1,20 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AppComponent} from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser";
import {AppComponent} from "./app.component";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {CoreModule} from "./components/core/core.module";
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
BrowserAnimationsModule,
CoreModule,
],
providers: [],
bootstrap: [AppComponent]
declarations: [
AppComponent,
],
imports: [
BrowserModule,
BrowserAnimationsModule,
CoreModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}

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

@ -1,19 +1,19 @@
#content {
height: 100vh;
width: 100vw;
position: absolute;
left: 0;
top: 0;
margin: 0;
overflow: hidden
height: 100vh;
width: 100vw;
position: absolute;
left: 0;
top: 0;
margin: 0;
overflow: hidden
}
mat-tab-group {
height: 100%;
width: 100%;
height: 100%;
width: 100%;
}
::ng-deep .mat-tab-body-wrapper {
height: 100%;
width: 100%;
height: 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', () => {
let component: CoreComponent;
let fixture: ComponentFixture<CoreComponent>;
describe("HomeComponent", () => {
let component: CoreComponent;
let fixture: ComponentFixture<CoreComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CoreComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CoreComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(CoreComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(CoreComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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 {RepositoryService} from "../../services/repository/repository.service";
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";
@Component({
selector: 'app-core',
templateUrl: './core.component.html',
styleUrls: ['./core.component.scss']
selector: "app-core",
templateUrl: "./core.component.html",
styleUrls: ["./core.component.scss"]
})
export class CoreComponent implements OnInit {
public selectedRepository: Repository | undefined;
public selectedRepository: Repository | undefined;
@ViewChild("tabGroup") tabGroup!: MatTabGroup;
@ViewChild("tabGroup") tabGroup!: MatTabGroup;
constructor(
private tabService: TabService,
private repoService: RepositoryService,
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();
});
}
constructor(
private tabService: TabService,
private repoService: RepositoryService,
private tagService: TagService) {
}
public updateSelectedTab() {
if (!this.tabGroup) {
return;
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();
});
}
if (!this.selectedRepository) {
this.tabGroup.selectedIndex = 0;
} else if (this.tabGroup.selectedIndex === 0) {
this.tabGroup.selectedIndex = 1;
public updateSelectedTab() {
if (!this.tabGroup) {
return;
}
if (!this.selectedRepository) {
this.tabGroup.selectedIndex = 0;
} else if (this.tabGroup.selectedIndex === 0) {
this.tabGroup.selectedIndex = 1;
}
}
}
async loadRepoData() {
await this.tagService.loadTags();
}
async loadRepoData() {
await this.tagService.loadTags();
}
public onTabSelectionChange(event: MatTabChangeEvent): void {
this.tabService.setSelectedTab(event.index);
}
public onTabSelectionChange(event: MatTabChangeEvent): void {
this.tabService.setSelectedTab(event.index);
}
}

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

@ -1,41 +1,43 @@
<div class="sidebar-inner">
<mat-tab-group headerPosition="below">
<mat-tab label="Search">
<div class="search-tab-inner" fxLayout="column">
<div id="file-search-input">
<app-file-search #filesearch (searchEndEvent)="this.searchEndEvent.emit()"
(searchStartEvent)="this.searchStartEvent.emit()"
[availableTags]="this.allTags"></app-file-search>
</div>
<mat-divider fxFlex="1em"></mat-divider>
<div class="tag-list-header" fxFlex="40px">
<h2>Tags</h2>
<mat-divider></mat-divider>
</div>
<div class="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill>
<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"
matRipple>
<app-tag-item [tag]="tag"></app-tag-item>
<mat-tab-group headerPosition="below">
<mat-tab label="Search">
<div class="search-tab-inner" fxLayout="column">
<div id="file-search-input">
<app-file-search #filesearch (searchEndEvent)="this.searchEndEvent.emit()"
(searchStartEvent)="this.searchStartEvent.emit()"
[availableTags]="this.allTags"></app-file-search>
</div>
<mat-divider fxFlex="1em"></mat-divider>
<div class="tag-list-header" fxFlex="40px">
<h2>Tags</h2>
<mat-divider></mat-divider>
</div>
<div class="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill>
<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"
matRipple>
<app-tag-item [tag]="tag"></app-tag-item>
</div>
</cdk-virtual-scroll-viewport>
</div>
</div>
</cdk-virtual-scroll-viewport>
</div>
</div>
</mat-tab>
<mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit">
<app-file-edit #fileedit [files]="this.selectedFiles"></app-file-edit>
</mat-tab>
</mat-tab-group>
</mat-tab>
<mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit">
<app-file-edit #fileedit [files]="this.selectedFiles"></app-file-edit>
</mat-tab>
</mat-tab-group>
</div>
<app-context-menu #contextMenu>
<button (click)="this.copyToClipboard(this.contextMenuTag!.getNormalizedOutput())" *ngIf="this.contextMenuTag"
mat-menu-item>Copy
"{{contextMenuTag!.getNormalizedOutput()}}"
</button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.name)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.name}}"
</button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.namespace!)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.namespace!}}"
</button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.getNormalizedOutput())" *ngIf="this.contextMenuTag"
mat-menu-item>Copy
"{{contextMenuTag!.getNormalizedOutput()}}"
</button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.name)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.name}}"
</button>
<button (click)="this.copyToClipboard(this.contextMenuTag!.namespace!)" *ngIf="this.contextMenuTag?.namespace"
mat-menu-item>Copy "{{this.contextMenuTag!.namespace!}}"
</button>
</app-context-menu>

@ -1,70 +1,70 @@
app-file-search {
display: block;
width: 100%;
display: block;
width: 100%;
}
#file-search-input {
width: 100%;
overflow: hidden;
width: 100%;
overflow: hidden;
}
mat-tab-group, mat-tab, .file-tag-list, app-file-edit {
height: 100%;
width: 100%;
height: 100%;
width: 100%;
}
mat-selection-list {
height: 100%;
user-select: none;
height: 100%;
user-select: none;
}
.sidebar-inner, .search-tab-inner {
height: 100%;
width: 100%;
display: block;
height: 100%;
width: 100%;
display: block;
}
.selectable-tag {
height: 50px;
display: flex;
font-size: 1.2em;
cursor: pointer;
transition-duration: 0.1s;
user-select: none;
height: 50px;
display: flex;
font-size: 1.2em;
cursor: pointer;
transition-duration: 0.1s;
user-select: none;
app-tag-item {
margin: auto auto auto 0.25em;
}
app-tag-item {
margin: auto auto auto 0.25em;
}
}
.selectable-tag:hover {
background-color: darken(dimgrey, 10);
background-color: darken(dimgrey, 10);
}
.selectable-tag:active {
cursor: pointer;
cursor: pointer;
}
cdk-virtual-scroll-viewport {
height: 100%;
width: 100%;
overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
height: 100%;
width: 100%;
}
overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%;
}
}
mat-divider {
width: 100%;
width: 100%;
}
.tag-list-header {
width: 100%;
display: flex;
flex-direction: column;
width: 100%;
display: flex;
flex-direction: column;
h2 {
margin: auto;
}
h2 {
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', () => {
let component: FilesTabSidebarComponent;
let fixture: ComponentFixture<FilesTabSidebarComponent>;
describe("FilesTabSidebarComponent", () => {
let component: FilesTabSidebarComponent;
let fixture: ComponentFixture<FilesTabSidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilesTabSidebarComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilesTabSidebarComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilesTabSidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilesTabSidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

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

@ -1,12 +1,12 @@
<mat-drawer-container autosize>
<mat-drawer disableClose mode="side" opened>
<app-files-tab-sidebar (searchEndEvent)="this.contentLoading = false"
(searchStartEvent)="this.contentLoading = true"
[selectedFiles]="this.selectedFiles"></app-files-tab-sidebar>
</mat-drawer>
<mat-drawer-content>
<app-busy-indicator [blurBackground]="true" [busy]="contentLoading">
<app-file-multiview (fileSelectEvent)="this.onFileSelect($event)" [files]="this.files"></app-file-multiview>
</app-busy-indicator>
</mat-drawer-content>
<mat-drawer disableClose mode="side" opened>
<app-files-tab-sidebar (searchEndEvent)="this.contentLoading = false"
(searchStartEvent)="this.contentLoading = true"
[selectedFiles]="this.selectedFiles"></app-files-tab-sidebar>
</mat-drawer>
<mat-drawer-content>
<app-busy-indicator [blurBackground]="true" [busy]="contentLoading">
<app-file-multiview (fileSelectEvent)="this.onFileSelect($event)" [files]="this.files"></app-file-multiview>
</app-busy-indicator>
</mat-drawer-content>
</mat-drawer-container>

@ -1,45 +1,45 @@
mat-selection-list {
height: 100%;
height: 100%;
}
mat-drawer {
height: 100%;
width: 25%;
overflow: hidden;
height: 100%;
width: 25%;
overflow: hidden;
}
mat-drawer-content {
overflow-x: hidden;
overflow-y: auto;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
height: 100%;
}
mat-drawer-container {
height: 100%;
width: 100%;
overflow: hidden;
height: 100%;
width: 100%;
overflow: hidden;
}
app-file-multiview {
padding: 0;
height: 100%;
width: 100%;
padding: 0;
height: 100%;
width: 100%;
}
.spinner-overlay {
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 998;
overflow: hidden;
display: flex;
backdrop-filter: blur(5px);
position: absolute;
top: 0;
right: 0;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 998;
overflow: hidden;
display: flex;
backdrop-filter: blur(5px);
mat-progress-spinner {
z-index: 999;
margin: auto;
}
mat-progress-spinner {
z-index: 999;
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', () => {
let component: FilesTabComponent;
let fixture: ComponentFixture<FilesTabComponent>;
describe("SearchPageComponent", () => {
let component: FilesTabComponent;
let fixture: ComponentFixture<FilesTabComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilesTabComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilesTabComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilesTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilesTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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 {ErrorBrokerService} from "../../../services/error-broker/error-broker.service";
import {FileService} from "../../../services/file/file.service";
import {RepositoryService} from "../../../services/repository/repository.service";
@Component({
selector: 'app-files-tab',
templateUrl: './files-tab.component.html',
styleUrls: ['./files-tab.component.scss']
selector: "app-files-tab",
templateUrl: "./files-tab.component.html",
styleUrls: ["./files-tab.component.scss"]
})
export class FilesTabComponent implements OnInit {
files: File[] = [];
contentLoading = false;
selectedFiles: File[] = [];
files: File[] = [];
contentLoading = false;
selectedFiles: File[] = [];
constructor(
private errorBroker: ErrorBrokerService,
private repoService: RepositoryService,
private fileService: FileService,) {
}
constructor(
private errorBroker: ErrorBrokerService,
private repoService: RepositoryService,
private fileService: FileService,) {
}
async ngOnInit() {
this.fileService.displayedFiles.subscribe(async (files) => {
this.files = files;
});
}
async ngOnInit() {
this.fileService.displayedFiles.subscribe(async (files) => {
this.files = files;
});
}
async onFileSelect(files: File[]) {
this.selectedFiles = files;
}
async onFileSelect(files: File[]) {
this.selectedFiles = files;
}
}

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

@ -1,25 +1,25 @@
app-native-file-select {
width: 100%;
width: 100%;
}
button {
width: 100%;
width: 100%;
}
mat-divider {
width: 100%;
height: 1px;
margin: 1em 0;
width: 100%;
height: 1px;
margin: 1em 0;
}
.filled-button {
background-color: #5c5c5c;
background-color: #5c5c5c;
}
.binary-import-options {
margin-top: 1em;
margin-top: 1em;
mat-checkbox {
margin: 0.5em 0;
}
mat-checkbox {
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', () => {
let component: FilesystemImportComponent;
let fixture: ComponentFixture<FilesystemImportComponent>;
describe("FilesystemImportComponent", () => {
let component: FilesystemImportComponent;
let fixture: ComponentFixture<FilesystemImportComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilesystemImportComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilesystemImportComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilesystemImportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilesystemImportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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 {ImportService} from "../../../../../services/import/import.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";
@Component({
selector: 'app-filesystem-import',
templateUrl: './filesystem-import.component.html',
styleUrls: ['./filesystem-import.component.scss']
selector: "app-filesystem-import",
templateUrl: "./filesystem-import.component.html",
styleUrls: ["./filesystem-import.component.scss"]
})
export class FilesystemImportComponent {
@Output() fileImported = new EventEmitter<File>();
@Output() importFinished = new EventEmitter<void>();
@Output() fileImported = new EventEmitter<File>();
@Output() importFinished = new EventEmitter<void>();
public fileCount: number = 0;
public files: FileOsMetadata[] = [];
public importOptions = new AddFileOptions();
public filters: DialogFilter[] = [
{name: "Images", extensions: ["png", "jpg", "jpeg", "webp", "bmp", "gif"]},
{name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]},
{name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"]},
{name: "Documents", extensions: ["pdf", "doc", "docx", "odf"]},
{name: "Text", extensions: ["txt", "md"]},
{name: "All", extensions: ["*"]}
]
public fileCount: number = 0;
public files: FileOsMetadata[] = [];
public importOptions = new AddFileOptions();
public filters: DialogFilter[] = [
{
name: "Images",
extensions: ["png", "jpg", "jpeg", "webp", "bmp", "gif"]
},
{name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]},
{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 importing = false;
public importingProgress = 0;
public resolving = false;
public importing = false;
public importingProgress = 0;
constructor(private errorBroker: ErrorBrokerService, private importService: ImportService) {
}
constructor(private errorBroker: ErrorBrokerService, private importService: ImportService) {
}
public async setSelectedPaths(paths: string[]) {
this.resolving = true;
try {
this.files = await this.importService.resolvePathsToFiles(paths);
this.fileCount = this.files.length;
} catch (err) {
console.log(err);
this.errorBroker.showError(err);
public async setSelectedPaths(paths: string[]) {
this.resolving = true;
try {
this.files = await this.importService.resolvePathsToFiles(paths);
this.fileCount = this.files.length;
} catch (err) {
console.log(err);
this.errorBroker.showError(err);
}
this.resolving = false;
}
this.resolving = false;
}
public async import() {
this.importing = true;
public async import() {
this.importing = true;
this.importingProgress = 0;
let count = 0;
this.importingProgress = 0;
let count = 0;
for (const file of this.files) {
try {
const resultFile = await this.importService.addLocalFile(file,
this.importOptions);
this.fileImported.emit(resultFile);
} catch (err) {
console.log(err);
this.errorBroker.showError(err);
}
count++;
this.importingProgress = (count / this.fileCount) * 100;
}
for (const file of this.files) {
try {
const resultFile = await this.importService.addLocalFile(file,
this.importOptions);
this.fileImported.emit(resultFile);
} catch (err) {
console.log(err);
this.errorBroker.showError(err);
}
count++;
this.importingProgress = (count / this.fileCount) * 100;
}
this.importing = false;
this.importFinished.emit();
}
this.importing = false;
this.importFinished.emit();
}
}

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

@ -1,35 +1,35 @@
mat-tab-group, mat-tab {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.import-tab-inner {
display: block;
width: 100%;
height: 100%;
overflow: hidden;
display: block;
width: 100%;
height: 100%;
overflow: hidden;
}
.import-type-select-wrapper {
width: 100%;
width: 100%;
.import-type-select {
width: calc(100% - 2em);
height: calc(100% - 2em);
margin: 1em;
.import-type-select {
width: calc(100% - 2em);
height: calc(100% - 2em);
margin: 1em;
mat-select {
height: 100%;
mat-select {
height: 100%;
}
}
}
}
.import-sidebar-tab-inner {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.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', () => {
let component: ImportTabSidebarComponent;
let fixture: ComponentFixture<ImportTabSidebarComponent>;
describe("ImportTabSidebarComponent", () => {
let component: ImportTabSidebarComponent;
let fixture: ComponentFixture<ImportTabSidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ImportTabSidebarComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ImportTabSidebarComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ImportTabSidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ImportTabSidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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";
@Component({
selector: 'app-import-tab-sidebar',
templateUrl: './import-tab-sidebar.component.html',
styleUrls: ['./import-tab-sidebar.component.scss']
selector: "app-import-tab-sidebar",
templateUrl: "./import-tab-sidebar.component.html",
styleUrls: ["./import-tab-sidebar.component.scss"]
})
export class ImportTabSidebarComponent {
@Output() fileImported = new EventEmitter<File>();
@Output() importFinished = new EventEmitter<void>();
@Output() fileImported = new EventEmitter<File>();
@Output() importFinished = new EventEmitter<void>();
constructor() {
}
constructor() {
}
}

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

@ -1,27 +1,27 @@
mat-drawer-container {
height: 100%;
width: 100%;
margin: 0;
overflow: hidden;
height: 100%;
width: 100%;
margin: 0;
overflow: hidden;
}
mat-drawer-content {
overflow-x: hidden;
overflow-y: auto;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
height: 100%;
}
mat-drawer {
height: 100%;
width: 25%;
height: 100%;
width: 25%;
}
app-import-tab-sidebar, app-file-multiview {
height: 100%;
width: 100%;
margin: 0;
height: 100%;
width: 100%;
margin: 0;
}
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', () => {
let component: ImportTabComponent;
let fixture: ComponentFixture<ImportTabComponent>;
describe("ImportTabComponent", () => {
let component: ImportTabComponent;
let fixture: ComponentFixture<ImportTabComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ImportTabComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ImportTabComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ImportTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ImportTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

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

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

@ -1,26 +1,26 @@
form {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
}
.dialog-buttons {
display: block;
width: 100%;
display: block;
width: 100%;
button {
margin-left: 1em;
float: right
}
button {
margin-left: 1em;
float: right
}
.check-connection-button {
justify-self: right;
margin: 0;
float: left;
}
.check-connection-button {
justify-self: right;
margin: 0;
float: left;
}
}
.button-folder-select {
position: absolute;
top: -10px;
right: 0;
position: absolute;
top: -10px;
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', () => {
let component: AddRepositoryDialogComponent;
let fixture: ComponentFixture<AddRepositoryDialogComponent>;
describe("AddRepositoryDialogComponent", () => {
let component: AddRepositoryDialogComponent;
let fixture: ComponentFixture<AddRepositoryDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddRepositoryDialogComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AddRepositoryDialogComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddRepositoryDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddRepositoryDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -1,10 +1,10 @@
import {Component, Inject, OnInit} from '@angular/core';
import {Component, Inject, OnInit} from "@angular/core";
import {
AbstractControl,
FormControl,
FormGroup,
ValidationErrors,
Validators
AbstractControl,
FormControl,
FormGroup,
ValidationErrors,
Validators
} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {RepositoryService} from "../../../../services/repository/repository.service";
@ -13,129 +13,129 @@ import {dialog} from "@tauri-apps/api";
import {Repository} from "../../../../models/Repository";
@Component({
selector: 'app-add-repository-dialog',
templateUrl: './add-repository-dialog.component.html',
styleUrls: ['./add-repository-dialog.component.scss']
selector: "app-add-repository-dialog",
templateUrl: "./add-repository-dialog.component.html",
styleUrls: ["./add-repository-dialog.component.scss"]
})
export class AddRepositoryDialogComponent implements OnInit {
formGroup = new FormGroup({
name: new FormControl("My Repository", [Validators.required]),
repositoryType: new FormControl("local", [Validators.required]),
path: new FormControl("", [this.validatePath]),
address: new FormControl("", [this.validateAddress])
});
repositories: Repository[] = [];
onlineStatus = "Unknown";
localRepoExists = false;
constructor(
public repoService: RepositoryService,
public errorBroker: ErrorBrokerService,
public dialogRef: MatDialogRef<AddRepositoryDialogComponent>,
@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);
formGroup = new FormGroup({
name: new FormControl("My Repository", [Validators.required]),
repositoryType: new FormControl("local", [Validators.required]),
path: new FormControl("", [this.validatePath]),
address: new FormControl("", [this.validateAddress])
});
repositories: Repository[] = [];
onlineStatus = "Unknown";
localRepoExists = false;
constructor(
public repoService: RepositoryService,
public errorBroker: ErrorBrokerService,
public dialogRef: MatDialogRef<AddRepositoryDialogComponent>,
@Inject(MAT_DIALOG_DATA) data: any) {
}
}
public closeDialog() {
this.dialogRef.close();
}
ngOnInit(): void {
this.repoService.repositories.subscribe(
repositories => this.repositories = repositories)
}
public async openFolderDialog() {
const path = await dialog.open({
directory: true,
multiple: false,
});
this.formGroup.get("path")?.setValue(path);
await this.checkLocalRepoExists();
}
public async onTypeChange(type: string) {
setTimeout(() => {
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 (this.repositories.find(r => r.name === value)) {
control?.setErrors({nameAlreadyExists: value});
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() {
this.dialogRef.close();
}
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 {
const repositoryType = control.parent?.get(
"repositoryType")?.value ?? "local";
public async onTypeChange(type: string) {
setTimeout(() => {
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") {
return control.value.length > 0 ? null : {valueRequired: control.value};
if (this.repositories.find(r => r.name === value)) {
control?.setErrors({nameAlreadyExists: value});
}
}
return null;
}
validateAddress(control: AbstractControl): ValidationErrors | null {
const repositoryType = control.parent?.get(
"repositoryType")?.value ?? "remote";
validatePath(control: AbstractControl): ValidationErrors | null {
const repositoryType = control.parent?.get(
"repositoryType")?.value ?? "local";
if (repositoryType === "remote") {
const match = /(\d+\.){3}\d+:\d+|\S+:\d+/.test(control.value)
return match ? null : {invalidAddress: control.value};
if (repositoryType === "local") {
return control.value.length > 0 ? null : {valueRequired: 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="add-repo-tools">
<button (click)="openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
</div>
<div class="repository-list">
<div *ngFor="let repository of repositories" class="repository-container">
<app-repository-card [repository]="repository"></app-repository-card>
<div class="add-repo-tools">
<button (click)="openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
</div>
<div class="repository-list">
<div *ngFor="let repository of repositories" class="repository-container">
<app-repository-card [repository]="repository"></app-repository-card>
</div>
</div>
</div>
</div>

@ -2,31 +2,31 @@
.repository-container {
margin: 1em;
margin: 1em;
}
.repo-page-content {
margin: 0 10%;
height: calc(100% - 2em);
margin: 0 10%;
height: calc(100% - 2em);
}
.add-repo-tools {
height: 5em;
display: flex;
flex-direction: row-reverse;
height: 5em;
display: flex;
flex-direction: row-reverse;
button {
margin: 1em;
}
button {
margin: 1em;
}
}
.repository-list {
display: flex;
flex-direction: column;
overflow-y: auto;
height: calc(100% - 5em);
display: flex;
flex-direction: column;
overflow-y: auto;
height: calc(100% - 5em);
}
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', () => {
let component: RepositoriesTabComponent;
let fixture: ComponentFixture<RepositoriesTabComponent>;
describe("RepositoriesComponent", () => {
let component: RepositoriesTabComponent;
let fixture: ComponentFixture<RepositoriesTabComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RepositoriesTabComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RepositoriesTabComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RepositoriesTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(RepositoriesTabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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 {RepositoryService} from "../../../services/repository/repository.service";
import {MatDialog} from "@angular/material/dialog";
import {AddRepositoryDialogComponent} from "./add-repository-dialog/add-repository-dialog.component";
@Component({
selector: 'app-repositories-tab',
templateUrl: './repositories-tab.component.html',
styleUrls: ['./repositories-tab.component.scss']
selector: "app-repositories-tab",
templateUrl: "./repositories-tab.component.html",
styleUrls: ["./repositories-tab.component.scss"]
})
export class RepositoriesTabComponent implements OnInit {
repositories: Repository[] = [];
repositories: Repository[] = [];
constructor(
private repoService: RepositoryService,
public dialog: MatDialog
) {
}
constructor(
private repoService: RepositoryService,
public dialog: MatDialog
) {
}
ngOnInit(): void {
this.repoService.repositories.subscribe({
next: (repos) => {
this.repositories = repos;
}
});
}
ngOnInit(): void {
this.repoService.repositories.subscribe({
next: (repos) => {
this.repositories = repos;
}
});
}
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
}
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
}
}

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

@ -1,40 +1,40 @@
@import "../../../../../styles";
.repository-path {
color: lightgray;
color: lightgray;
}
.repository-address {
color: lightgray;
font-family: "Fira Code Light", Monospaced, Consolas, monospace;
color: lightgray;
font-family: "Fira Code Light", Monospaced, Consolas, monospace;
}
.repository-status {
position: absolute;
right: 0;
top: 0;
padding: 0.5em;
width: 3em;
text-align: center;
border-bottom-left-radius: 1em;
p {
margin: auto;
}
position: absolute;
right: 0;
top: 0;
padding: 0.5em;
width: 3em;
text-align: center;
border-bottom-left-radius: 1em;
p {
margin: auto;
}
}
.status-local {
background-color: #2e237a;
background-color: #2e237a;
}
.status-offline {
background-color: #8e1e2a;
background-color: #8e1e2a;
}
.status-online {
background-color: #1b651b;
background-color: #1b651b;
}
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', () => {
let component: RepositoryCardComponent;
let fixture: ComponentFixture<RepositoryCardComponent>;
describe("RepositoryCardComponent", () => {
let component: RepositoryCardComponent;
let fixture: ComponentFixture<RepositoryCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RepositoryCardComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [RepositoryCardComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RepositoryCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(RepositoryCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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 {RepositoryService} from "../../../../services/repository/repository.service";
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";
@Component({
selector: 'app-repository-card',
templateUrl: './repository-card.component.html',
styleUrls: ['./repository-card.component.scss']
selector: "app-repository-card",
templateUrl: "./repository-card.component.html",
styleUrls: ["./repository-card.component.scss"]
})
export class RepositoryCardComponent implements OnInit, OnDestroy {
@Input() repository!: Repository;
@ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent;
@Input() repository!: Repository;
@ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent;
public daemonRunning: boolean = false;
public daemonRunning: boolean = false;
statusRefreshInterval: number | undefined;
statusRefreshInterval: number | undefined;
constructor(
public repoService: RepositoryService,
private router: Router,
private errorBroker: ErrorBrokerService,
public dialog: MatDialog) {
}
constructor(
public repoService: RepositoryService,
private router: Router,
private errorBroker: ErrorBrokerService,
public dialog: MatDialog) {
}
public async ngOnInit() {
if (!this.repository.local) {
await this.checkRemoteRepositoryStatus();
this.statusRefreshInterval = setInterval(
async () => await this.checkRemoteRepositoryStatus(), 10000);
public async ngOnInit() {
if (!this.repository.local) {
await this.checkRemoteRepositoryStatus();
this.statusRefreshInterval = setInterval(
async () => await this.checkRemoteRepositoryStatus(), 10000);
}
}
}
public async ngOnDestroy(): Promise<void> {
if (this.statusRefreshInterval != undefined) {
clearInterval(this.statusRefreshInterval);
public async ngOnDestroy(): Promise<void> {
if (this.statusRefreshInterval != undefined) {
clearInterval(this.statusRefreshInterval);
}
}
}
public isSelectedRepository(): boolean {
return this.repoService.selectedRepository.getValue()?.name === this.repository.name
}
public isSelectedRepository(): boolean {
return this.repoService.selectedRepository.getValue()?.name === this.repository.name
}
public async removeRepository() {
await this.dialog.open(ConfirmDialogComponent, {
data: {
title: "Remove repository",
message: `Do you really want to remove the repository "${this.repository.name}"?`,
confirmAction: "Remove",
confirmColor: "warn"
}
}).afterClosed().subscribe(async confirmation => {
if (confirmation === true) {
await this.repoService.removeRepository(this.repository.name);
}
});
}
public async removeRepository() {
await this.dialog.open(ConfirmDialogComponent, {
data: {
title: "Remove repository",
message: `Do you really want to remove the repository "${this.repository.name}"?`,
confirmAction: "Remove",
confirmColor: "warn"
}
}).afterClosed().subscribe(async confirmation => {
if (confirmation === true) {
await this.repoService.removeRepository(this.repository.name);
}
});
}
public getDaemonStatusText(): string {
if (this.repository.local) {
return "Local";
} else if (this.daemonRunning) {
return "Online";
} else {
return "Offline";
public getDaemonStatusText(): string {
if (this.repository.local) {
return "Local";
} else if (this.daemonRunning) {
return "Online";
} else {
return "Offline";
}
}
}
public getDaemonStatusClass(): string {
if (this.repository.local) {
return "status-local";
} else if (this.daemonRunning) {
return "status-online";
} else {
return "status-offline";
public getDaemonStatusClass(): string {
if (this.repository.local) {
return "status-local";
} else if (this.daemonRunning) {
return "status-online";
} else {
return "status-offline";
}
}
}
public async startDaemonAndSelectRepository() {
try {
if (!this.daemonRunning) {
await this.repoService.startDaemon(this.repository.path!);
this.daemonRunning = true;
await new Promise((res, _) => {
setTimeout(res, 2000) // wait for the daemon to start
});
}
await this.selectRepository();
} catch (err) {
this.errorBroker.showError(err);
public async startDaemonAndSelectRepository() {
try {
if (!this.daemonRunning) {
await this.repoService.startDaemon(this.repository.path!);
this.daemonRunning = true;
await new Promise((res, _) => {
setTimeout(res, 2000) // wait for the daemon to start
});
}
await this.selectRepository();
} catch (err) {
this.errorBroker.showError(err);
}
}
}
public async selectRepository() {
this.busyIndicator.setBusy(true);
try {
await this.repoService.setRepository(this.repository);
} catch (err) {
this.errorBroker.showError(err);
public async selectRepository() {
this.busyIndicator.setBusy(true);
try {
await this.repoService.setRepository(this.repository);
} catch (err) {
this.errorBroker.showError(err);
}
this.busyIndicator.setBusy(false);
}
this.busyIndicator.setBusy(false);
}
async checkRemoteRepositoryStatus() {
this.daemonRunning = await this.repoService.checkDaemonRunning(
this.repository.address!);
}
async checkRemoteRepositoryStatus() {
this.daemonRunning = await this.repoService.checkDaemonRunning(
this.repository.address!);
}
}

@ -1,5 +1,5 @@
<ng-content></ng-content>
<div *ngIf="this.busy" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground"
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>

@ -1,31 +1,31 @@
.busy-indicator-overlay {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
z-index: 998;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
z-index: 998;
mat-progress-spinner {
z-index: 999;
margin: auto;
}
mat-progress-spinner {
z-index: 999;
margin: auto;
}
}
.busy-indicator-overlay.blur {
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.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 {
width: 100%;
height: 100%;
position: relative;
display: block;
width: 100%;
height: 100%;
position: relative;
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', () => {
let component: BusyIndicatorComponent;
let fixture: ComponentFixture<BusyIndicatorComponent>;
describe("BusyIndicatorComponent", () => {
let component: BusyIndicatorComponent;
let fixture: ComponentFixture<BusyIndicatorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BusyIndicatorComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BusyIndicatorComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(BusyIndicatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(BusyIndicatorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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";
@Component({
selector: 'app-busy-indicator',
templateUrl: './busy-indicator.component.html',
styleUrls: ['./busy-indicator.component.scss']
selector: "app-busy-indicator",
templateUrl: "./busy-indicator.component.html",
styleUrls: ["./busy-indicator.component.scss"]
})
export class BusyIndicatorComponent {
@Input() busy: boolean = false;
@Input() blurBackground: boolean = false;
@Input() darkenBackground: boolean = false;
@Input() mode: ProgressSpinnerMode = "indeterminate";
@Input() value: number | undefined;
@Input() busy: boolean = false;
@Input() blurBackground: boolean = false;
@Input() darkenBackground: boolean = false;
@Input() mode: ProgressSpinnerMode = "indeterminate";
@Input() value: number | undefined;
constructor() {
}
constructor() {
}
public setBusy(busy: boolean) {
this.busy = busy;
}
public setBusy(busy: boolean) {
this.busy = busy;
}
public wrapOperation<T>(operation: Function): T | undefined {
this.setBusy(true)
try {
const result = operation();
this.setBusy(false);
return result;
} catch {
return undefined;
} finally {
this.setBusy(false);
public wrapOperation<T>(operation: Function): T | undefined {
this.setBusy(true)
try {
const result = operation();
this.setBusy(false);
return result;
} catch {
return undefined;
} finally {
this.setBusy(false);
}
}
}
public async wrapAsyncOperation<T>(operation: Function): Promise<T | undefined> {
this.setBusy(true)
try {
const result = await operation();
this.setBusy(false);
return result;
} catch {
return undefined;
} finally {
this.setBusy(false);
public async wrapAsyncOperation<T>(operation: Function): Promise<T | undefined> {
this.setBusy(true)
try {
const result = await operation();
this.setBusy(false);
return result;
} catch {
return undefined;
} finally {
this.setBusy(false);
}
}
}
}

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

@ -1,8 +1,8 @@
.confirm-dialog-actions {
display: block;
display: block;
button {
float: right;
margin-left: 1em;
}
button {
float: right;
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', () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
describe("ConfirmDialogComponent", () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ConfirmDialogComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ConfirmDialogComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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 {ThemePalette} from "@angular/material/core";
@Component({
selector: 'app-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
selector: "app-confirm-dialog",
templateUrl: "./confirm-dialog.component.html",
styleUrls: ["./confirm-dialog.component.scss"]
})
export class ConfirmDialogComponent {
title = "";
message = "";
confirmAction = "";
confirmColor: ThemePalette = "primary";
denyAction = "Cancel";
denyColor: ThemePalette = "accent";
title = "";
message = "";
confirmAction = "";
confirmColor: ThemePalette = "primary";
denyAction = "Cancel";
denyColor: ThemePalette = "accent";
constructor(
public dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject(
MAT_DIALOG_DATA) data: { title: string, message: string, confirmAction: string, denyAction?: string, confirmColor?: ThemePalette, denyColor?: ThemePalette }
) {
this.title = data.title;
this.message = data.message;
this.confirmAction = data.confirmAction;
this.denyAction = data.denyAction ?? this.denyAction;
this.confirmColor = data.confirmColor ?? this.confirmColor;
this.denyColor = data.denyColor ?? this.denyColor;
}
constructor(
public dialogRef: MatDialogRef<ConfirmDialogComponent>,
@Inject(
MAT_DIALOG_DATA) data: { title: string, message: string, confirmAction: string, denyAction?: string, confirmColor?: ThemePalette, denyColor?: ThemePalette }
) {
this.title = data.title;
this.message = data.message;
this.confirmAction = data.confirmAction;
this.denyAction = data.denyAction ?? this.denyAction;
this.confirmColor = data.confirmColor ?? this.confirmColor;
this.denyColor = data.denyColor ?? this.denyColor;
}
public closeDialog(result: boolean) {
this.dialogRef.close(result);
}
public closeDialog(result: boolean) {
this.dialogRef.close(result);
}
}

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

@ -1,21 +1,21 @@
.image-container {
height: 100%;
width: 100%;
display: flex;
height: 100%;
width: 100%;
display: flex;
}
img {
margin: auto;
max-height: 100%;
max-width: 100%;
height: auto;
width: auto;
margin: auto;
max-height: 100%;
max-width: 100%;
height: auto;
width: auto;
}
img.scale-height {
height: 100%;
height: 100%;
}
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', () => {
let component: ContentAwareImageComponent;
let fixture: ComponentFixture<ContentAwareImageComponent>;
describe("ContentAwareImageComponent", () => {
let component: ContentAwareImageComponent;
let fixture: ComponentFixture<ContentAwareImageComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContentAwareImageComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContentAwareImageComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentAwareImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentAwareImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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";
@Component({
selector: 'app-content-aware-image',
templateUrl: './content-aware-image.component.html',
styleUrls: ['./content-aware-image.component.scss']
selector: "app-content-aware-image",
templateUrl: "./content-aware-image.component.html",
styleUrls: ["./content-aware-image.component.scss"]
})
export class ContentAwareImageComponent implements OnInit {
@Input() imageSrc!: string | SafeResourceUrl;
@Input() maximizeHeight: boolean = true;
@Input() maximizeWidth: boolean = true;
@Input() borderRadius: string | undefined;
@Input() decoding: "async" | "sync" | "auto" = "auto";
@Input() imageSrc!: string | SafeResourceUrl;
@Input() maximizeHeight: boolean = true;
@Input() maximizeWidth: boolean = true;
@Input() borderRadius: string | undefined;
@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 {
if (this.image) {
this.image.nativeElement.decoding = this.decoding;
public ngOnInit(): void {
if (this.image) {
this.image.nativeElement.decoding = this.decoding;
}
}
}
/**
* Fits the image into the container
* @param {HTMLImageElement} image
* @param {HTMLDivElement} imageContainer
*/
public adjustSize(image: HTMLImageElement, imageContainer: HTMLDivElement): void {
const containerHeight = Math.abs(imageContainer.clientHeight);
const containerWidth = Math.abs(imageContainer.clientWidth);
const imageRelativeHeight = image.naturalHeight / containerHeight;
const imageRelativeWidth = image.naturalWidth / containerWidth;
this.scaleWidth = imageRelativeWidth > imageRelativeHeight;
}
/**
* Fits the image into the container
* @param {HTMLImageElement} image
* @param {HTMLDivElement} imageContainer
*/
public adjustSize(image: HTMLImageElement, imageContainer: HTMLDivElement): void {
const containerHeight = Math.abs(imageContainer.clientHeight);
const containerWidth = Math.abs(imageContainer.clientWidth);
const imageRelativeHeight = image.naturalHeight / containerHeight;
const imageRelativeWidth = image.naturalWidth / containerWidth;
this.scaleWidth = imageRelativeWidth > imageRelativeHeight;
}
}

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

@ -1,4 +1,4 @@
.menu-anchor {
visibility: hidden;
position: fixed;
visibility: hidden;
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', () => {
let component: ContextMenuComponent;
let fixture: ComponentFixture<ContextMenuComponent>;
describe("ContextMenuComponent", () => {
let component: ContextMenuComponent;
let fixture: ComponentFixture<ContextMenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContextMenuComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContextMenuComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ContextMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ContextMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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";
@Component({
selector: 'app-context-menu',
templateUrl: './context-menu.component.html',
styleUrls: ['./context-menu.component.scss']
selector: "app-context-menu",
templateUrl: "./context-menu.component.html",
styleUrls: ["./context-menu.component.scss"]
})
export class ContextMenuComponent {
public x: string = "0";
public y: string = "0";
public x: string = "0";
public y: string = "0";
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger
constructor() {
}
constructor() {
}
public onContextMenu(event: MouseEvent) {
event.preventDefault();
this.x = event.clientX + "px";
this.y = event.clientY + "px";
this.menuTrigger.menu.focusFirstItem("mouse");
this.menuTrigger.openMenu();
}
public onContextMenu(event: MouseEvent) {
event.preventDefault();
this.x = event.clientX + "px";
this.y = event.clientY + "px";
this.menuTrigger.menu.focusFirstItem("mouse");
this.menuTrigger.openMenu();
}
}

@ -1,5 +1,5 @@
<app-context-menu #contextMenu>
<button (click)="this.copyFileHash()" mat-menu-item>Copy Hash</button>
<button (click)="this.exportFile()" mat-menu-item>Save As...</button>
<ng-content></ng-content>
<button (click)="this.copyFileHash()" mat-menu-item>Copy Hash</button>
<button (click)="this.exportFile()" mat-menu-item>Save As...</button>
<ng-content></ng-content>
</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', () => {
let component: FileContextMenuComponent;
let fixture: ComponentFixture<FileContextMenuComponent>;
describe("FileContextMenuComponent", () => {
let component: FileContextMenuComponent;
let fixture: ComponentFixture<FileContextMenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileContextMenuComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileContextMenuComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileContextMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileContextMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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 {ContextMenuComponent} from "../context-menu.component";
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";
@Component({
selector: 'app-file-context-menu',
templateUrl: './file-context-menu.component.html',
styleUrls: ['./file-context-menu.component.scss']
selector: "app-file-context-menu",
templateUrl: "./file-context-menu.component.html",
styleUrls: ["./file-context-menu.component.scss"]
})
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) {
this.file = file;
this.contextMenu.onContextMenu(event);
}
public onContextMenu(event: MouseEvent, file: File) {
this.file = file;
this.contextMenu.onContextMenu(event);
}
public async copyFileHash(): Promise<void> {
await clipboard.writeText(this.file.hash);
}
public async copyFileHash(): Promise<void> {
await clipboard.writeText(this.file.hash);
}
public async exportFile(): Promise<void> {
const path = await FileHelper.getFileDownloadLocation(this.file)
public async exportFile(): Promise<void> {
const path = await FileHelper.getFileDownloadLocation(this.file)
if (path) {
try {
await this.fileService.saveFile(this.file, path);
} catch (err) {
this.errorBroker.showError(err);
}
if (path) {
try {
await this.fileService.saveFile(this.file, path);
} catch (err) {
this.errorBroker.showError(err);
}
}
}
}
}

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

@ -1,69 +1,69 @@
.file-metadata, .tag-input {
width: 100%;
mat-form-field, app-tag-input {
width: 100%;
}
mat-form-field.form-field-mode {
width: 10em;
}
mat-form-field, app-tag-input {
width: 100%;
}
mat-form-field.form-field-mode {
width: 10em;
}
}
.file-edit-inner {
height: 100%;
width: 100%;
display: block;
height: 100%;
width: 100%;
display: block;
}
.tag-edit-list {
height: 100%;
width: 100%;
display: block;
overflow: hidden;
height: 100%;
width: 100%;
display: block;
overflow: hidden;
}
cdk-virtual-scroll-viewport {
height: 100%;
width: 100%;
overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
height: 100%;
width: 100%;
}
overflow-y: auto;
::ng-deep .cdk-virtual-scroll-content-wrapper {
width: 100%;
}
}
.editable-tag {
height: 50px;
width: 100%;
display: flex;
font-size: 1.2em;
transition-duration: 0.1s;
user-select: none;
overflow: hidden;
cursor: default;
height: 50px;
width: 100%;
display: flex;
font-size: 1.2em;
transition-duration: 0.1s;
user-select: none;
overflow: hidden;
cursor: default;
app-tag-item {
margin: auto auto auto 0.25em;
}
app-tag-item {
margin: auto auto auto 0.25em;
}
.tag-remove-button {
margin-right: 1em;
height: 50px;
width: 50px;
}
.tag-remove-button {
margin-right: 1em;
height: 50px;
width: 50px;
}
}
.tag-input-field {
display: flex;
flex-direction: row;
display: flex;
flex-direction: row;
.add-tag-button {
width: 65px;
height: 65px;
}
.add-tag-button {
width: 65px;
height: 65px;
}
}
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', () => {
let component: FileEditComponent;
let fixture: ComponentFixture<FileEditComponent>;
describe("FileEditComponent", () => {
let component: FileEditComponent;
let fixture: ComponentFixture<FileEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileEditComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileEditComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

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

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

@ -1,9 +1,9 @@
.audio-container {
height: 100%;
width: 100%;
display: flex;
height: 100%;
width: 100%;
display: flex;
audio {
margin: auto;
}
audio {
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', () => {
let component: AudioViewerComponent;
let fixture: ComponentFixture<AudioViewerComponent>;
describe("AudioViewerComponent", () => {
let component: AudioViewerComponent;
let fixture: ComponentFixture<AudioViewerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AudioViewerComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AudioViewerComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AudioViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(AudioViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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";
@Component({
selector: 'app-audio-viewer',
templateUrl: './audio-viewer.component.html',
styleUrls: ['./audio-viewer.component.scss']
selector: "app-audio-viewer",
templateUrl: "./audio-viewer.component.html",
styleUrls: ["./audio-viewer.component.scss"]
})
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>
<div *ngIf="getContentType() === 'other'" class="download-prompt">
<span>Unsupported content type <b>{{this.file.mime_type}}</b></span>
<button (click)="this.downloadContent()" color="primary" mat-flat-button>Download</button>
<span>Unsupported content type <b>{{this.file.mime_type}}</b></span>
<button (click)="this.downloadContent()" color="primary" mat-flat-button>Download</button>
</div>
<app-busy-indicator></app-busy-indicator>

@ -1,21 +1,21 @@
app-image-viewer, app-video-viewer, app-audio-viewer {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.download-prompt {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
button {
margin: 1em 0 auto;
align-self: center;
}
button {
margin: 1em 0 auto;
align-self: center;
}
span {
margin: auto 0 0;
align-self: center;
}
span {
margin: auto 0 0;
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', () => {
let component: ContentViewerComponent;
let fixture: ComponentFixture<ContentViewerComponent>;
describe("ContentViewerComponent", () => {
let component: ContentViewerComponent;
let fixture: ComponentFixture<ContentViewerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContentViewerComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ContentViewerComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ContentViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

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

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

@ -1,35 +1,35 @@
.image-drag-container, .image-scale-container {
height: 100%;
width: 100%;
height: 100%;
width: 100%;
}
.image-scale-container {
display: block;
display: block;
}
.zoom-slider {
position: absolute;
display: flex;
flex-direction: column;
right: 1em;
bottom: 1em;
z-index: 10;
opacity: 0.5;
padding: 1em 0.5em;
transition-duration: 0.2s;
position: absolute;
display: flex;
flex-direction: column;
right: 1em;
bottom: 1em;
z-index: 10;
opacity: 0.5;
padding: 1em 0.5em;
transition-duration: 0.2s;
}
.zoom-slider:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 1em;
opacity: 1;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 1em;
}
.image-full-view-inner {
height: 100%;
width: 100%;
display: block;
position: relative;
height: 100%;
width: 100%;
display: block;
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', () => {
let component: ImageViewerComponent;
let fixture: ComponentFixture<ImageViewerComponent>;
describe("ImageViewerComponent", () => {
let component: ImageViewerComponent;
let fixture: ComponentFixture<ImageViewerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ImageViewerComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ImageViewerComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ImageViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(ImageViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -1,69 +1,69 @@
import {
Component,
HostListener,
Input,
OnChanges,
SimpleChanges
} from '@angular/core';
Component,
HostListener,
Input,
OnChanges,
SimpleChanges
} from "@angular/core";
import {CdkDragMove} from "@angular/cdk/drag-drop";
import {SafeResourceUrl} from "@angular/platform-browser";
@Component({
selector: 'app-image-viewer',
templateUrl: './image-viewer.component.html',
styleUrls: ['./image-viewer.component.scss']
selector: "app-image-viewer",
templateUrl: "./image-viewer.component.html",
styleUrls: ["./image-viewer.component.scss"]
})
export class ImageViewerComponent implements OnChanges {
@Input() imageUrl!: SafeResourceUrl | string;
public imageZoom = 1;
public imagePosition = {x: 0, y: 0};
public mouseInImageView = false;
@Input() imageUrl!: SafeResourceUrl | string;
public imageZoom = 1;
public imagePosition = {x: 0, y: 0};
public mouseInImageView = false;
constructor() {
}
constructor() {
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes["imageUrl"]) {
this.resetImage();
public ngOnChanges(changes: SimpleChanges): void {
if (changes["imageUrl"]) {
this.resetImage();
}
}
}
public resetImage() {
this.imageZoom = 1;
this.imagePosition = {x: 0, y: 0};
}
public resetImage() {
this.imageZoom = 1;
this.imagePosition = {x: 0, y: 0};
}
public onDragMoved($event: CdkDragMove<HTMLDivElement>): void {
this.imagePosition.x += $event.delta.x;
this.imagePosition.y += $event.delta.y;
}
public onDragMoved($event: CdkDragMove<HTMLDivElement>): void {
this.imagePosition.x += $event.delta.x;
this.imagePosition.y += $event.delta.y;
}
@HostListener("window:keydown", ["$event"])
private async handleKeydownEvent(event: KeyboardEvent) {
switch (event.key) {
case "Escape":
this.resetImage();
break;
@HostListener("window:keydown", ["$event"])
private async handleKeydownEvent(event: KeyboardEvent) {
switch (event.key) {
case "Escape":
this.resetImage();
break;
}
}
}
@HostListener("mousewheel", ["$event"])
private handleScroll(event: any) {
if (this.mouseInImageView) {
const delta = event.wheelDelta ?? event.detail;
@HostListener("mousewheel", ["$event"])
private handleScroll(event: any) {
if (this.mouseInImageView) {
const delta = event.wheelDelta ?? event.detail;
if (delta > 0) {
this.imageZoom += 0.2
if (this.imageZoom > 4) {
this.imageZoom = 4;
}
} else if (delta < 0) {
this.imageZoom -= 0.2
if (this.imageZoom < 0.5) {
this.imageZoom = 0.5;
if (delta > 0) {
this.imageZoom += 0.2
if (this.imageZoom > 4) {
this.imageZoom = 4;
}
} else if (delta < 0) {
this.imageZoom -= 0.2
if (this.imageZoom < 0.5) {
this.imageZoom = 0.5;
}
}
}
}
}
}
}

@ -1,3 +1,3 @@
<video [src]="this.blobUrl" controls>
Unsupported video type
Unsupported video type
</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', () => {
let component: VideoViewerComponent;
let fixture: ComponentFixture<VideoViewerComponent>;
describe("VideoViewerComponent", () => {
let component: VideoViewerComponent;
let fixture: ComponentFixture<VideoViewerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [VideoViewerComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [VideoViewerComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(VideoViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(VideoViewerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
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";
@Component({
selector: 'app-video-viewer',
templateUrl: './video-viewer.component.html',
styleUrls: ['./video-viewer.component.scss']
selector: "app-video-viewer",
templateUrl: "./video-viewer.component.html",
styleUrls: ["./video-viewer.component.scss"]
})
export class VideoViewerComponent {
@Input() blobUrl!: SafeResourceUrl;
@Input() blobUrl!: SafeResourceUrl;
}

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

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

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

@ -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', () => {
let component: FileGalleryEntryComponent;
let fixture: ComponentFixture<FileGalleryEntryComponent>;
describe("FileGalleryEntryComponent", () => {
let component: FileGalleryEntryComponent;
let fixture: ComponentFixture<FileGalleryEntryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileGalleryEntryComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileGalleryEntryComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileGalleryEntryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileGalleryEntryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

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

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

@ -1,62 +1,62 @@
.file-scroll-viewport {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
::ng-deep .file-scroll-viewport > .cdk-virtual-scroll-content-wrapper {
display: flex;
flex-direction: row;
height: 100%;
display: flex;
flex-direction: row;
height: 100%;
}
.gallery-container {
height: 100%;
width: 100%;
position: relative;
user-select: none;
height: 100%;
width: 100%;
position: relative;
user-select: none;
}
app-file-gallery-entry, .file-item {
width: 250px;
height: calc(100% - 10px);
padding: 5px;
width: 250px;
height: calc(100% - 10px);
padding: 5px;
}
app-file-gallery-entry {
display: block;
display: block;
}
.file-full-view {
width: 100%;
height: 100%;
overflow: hidden;
width: 100%;
height: 100%;
overflow: hidden;
}
app-content-viewer {
height: 100%;
width: 100%;
display: block;
height: 100%;
width: 100%;
display: block;
}
.close-button {
position: absolute;
top: 0;
right: 0;
width: 3em;
height: 3em;
z-index: 999;
position: absolute;
top: 0;
right: 0;
width: 3em;
height: 3em;
z-index: 999;
}
.url-loading-backdrop {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
mat-progress-spinner {
margin: auto;
}
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
mat-progress-spinner {
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', () => {
let component: FileGalleryComponent;
let fixture: ComponentFixture<FileGalleryComponent>;
describe("FileGalleryComponent", () => {
let component: FileGalleryComponent;
let fixture: ComponentFixture<FileGalleryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileGalleryComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileGalleryComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileGalleryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileGalleryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

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

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

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

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

@ -1,16 +1,16 @@
mat-card {
height: calc(100% - 32px);
width: calc(100% - 32px);
user-select: none;
cursor: pointer;
height: calc(100% - 32px);
width: calc(100% - 32px);
user-select: none;
cursor: pointer;
}
mat-card-content {
height: 100%;
width: 100%;
height: 100%;
width: 100%;
}
.entry-image {
width: 100%;
height: 100%;
width: 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', () => {
let component: FileGridEntryComponent;
let fixture: ComponentFixture<FileGridEntryComponent>;
describe("FileGridEntryComponent", () => {
let component: FileGridEntryComponent;
let fixture: ComponentFixture<FileGridEntryComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileGridEntryComponent]
})
.compileComponents();
});
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FileGridEntryComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileGridEntryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileGridEntryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

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

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

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

Loading…
Cancel
Save