Add extended filtering implementation

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 2 years ago
parent 8007bf64a7
commit 501a3c9df0

@ -9,8 +9,27 @@ insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.ts] [*.ts]
quote_type = double trim_trailing_whitespace = true
charset = utf-8
ij_typescript_align_multiline_chained_methods = true
ij_typescript_force_quote_style = true
ij_typescript_space_after_colon = true
ij_typescript_space_after_comma = true
ij_typescript_space_after_property_colon = true
ij_typescript_space_after_type_colon = true
ij_any_spaces_within_braces = true
ij_typescript_spaces_within_object_type_braces = true
ij_typescript_spaces_within_object_literal_braces = true
ij_any_call_parameters_wrap = on_every_item
ij_any_method_parameters_wrap = on_every_item
ij_any_method_call_chain_wrap = on_every_item
ij_typescript_method_parameters_wrap = on_every_item
ij_any_call_parameters_new_line_after_left_paren = true
ij_any_call_parameters_right_paren_on_new_line = true
ij_any_method_parameters_new_line_after_left_paren = true
ij_any_method_parameters_right_paren_on_new_line = true
max_line_length = 120
[*.md] [*.md]
max_line_length = off max_line_length = off
trim_trailing_whitespace = false trim_trailing_whitespace = false

@ -7,6 +7,9 @@
# Only exists if Bazel was run # Only exists if Bazel was run
/bazel-out /bazel-out
# angular stuff
.angular
# dependencies # dependencies
/node_modules /node_modules

@ -1,61 +1,60 @@
{ {
"name": "mediarepo-ui", "name": "mediarepo-ui",
"version": "0.12.0", "version": "0.12.0",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"tauri": "tauri" "tauri": "tauri"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~12.2.0", "@angular/animations": "~13.1.1",
"@angular/cdk": "12.2.9", "@angular/cdk": "^13.1.1",
"@angular/common": "~12.2.0", "@angular/common": "~13.1.1",
"@angular/compiler": "~12.2.0", "@angular/compiler": "~13.1.1",
"@angular/core": "~12.2.0", "@angular/core": "~13.1.1",
"@angular/flex-layout": "^12.0.0-beta.35", "@angular/flex-layout": "^13.0.0-beta.36",
"@angular/forms": "~12.2.0", "@angular/forms": "~13.1.1",
"@angular/material": "12.2.9", "@angular/material": "^13.1.1",
"@angular/platform-browser": "~12.2.0", "@angular/platform-browser": "~13.1.1",
"@angular/platform-browser-dynamic": "~12.2.0", "@angular/platform-browser-dynamic": "~13.1.1",
"@angular/router": "~12.2.0", "@angular/router": "~13.1.1",
"@ng-icons/core": "^13.1.1", "@ng-icons/core": "^13.2.0",
"@ng-icons/feather-icons": "^13.1.1", "@ng-icons/feather-icons": "^13.2.0",
"@ng-icons/material-icons": "^13.1.1", "@ng-icons/material-icons": "13.1.0",
"@tauri-apps/api": "^1.0.0-beta.8", "@tauri-apps/api": "^1.0.0-beta.8",
"ngx-lightbox": "^2.5.1", "primeicons": "^5.0.0",
"primeicons": "^4.1.0", "primeng": "^13.0.4",
"primeng": "^12.2.1", "rxjs": "~7.5.2",
"rxjs": "~6.6.0", "tslib": "^2.3.1",
"tslib": "^2.3.0", "zone.js": "~0.11.4"
"zone.js": "~0.11.4" },
}, "devDependencies": {
"devDependencies": { "@angular-devkit/build-angular": "~13.1.2",
"@angular-devkit/build-angular": "~12.2.9", "@angular-eslint/builder": "^13.0.1",
"@angular-eslint/builder": "12.5.0", "@angular-eslint/eslint-plugin": "^13.0.1",
"@angular-eslint/eslint-plugin": "12.5.0", "@angular-eslint/eslint-plugin-template": "^13.0.1",
"@angular-eslint/eslint-plugin-template": "12.5.0", "@angular-eslint/schematics": "^13.0.1",
"@angular-eslint/schematics": "12.5.0", "@angular-eslint/template-parser": "^13.0.1",
"@angular-eslint/template-parser": "12.5.0", "@angular/cli": "~13.1.2",
"@angular/cli": "~12.2.9", "@angular/compiler-cli": "~13.1.1",
"@angular/compiler-cli": "~12.2.0", "@tauri-apps/cli": "^1.0.0-beta.10",
"@tauri-apps/cli": "^1.0.0-beta.10", "@types/file-saver": "^2.0.4",
"@types/file-saver": "^2.0.3", "@types/jasmine": "~3.10.3",
"@types/jasmine": "~3.8.0", "@types/node": "^16.11.19",
"@types/node": "^12.11.1", "@typescript-eslint/eslint-plugin": "5.9.1",
"@typescript-eslint/eslint-plugin": "4.28.2", "@typescript-eslint/parser": "^5.9.1",
"@typescript-eslint/parser": "4.28.2", "eslint": "^8.6.0",
"eslint": "^7.26.0", "jasmine-core": "~4.0.0",
"jasmine-core": "~3.8.0", "karma": "~6.3.10",
"karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0",
"karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.1.0",
"karma-coverage": "~2.0.3", "karma-jasmine": "~4.0.1",
"karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "~1.7.0",
"karma-jasmine-html-reporter": "~1.7.0", "typescript": "~4.5.4"
"typescript": "~4.3.5" }
}
} }

@ -1489,8 +1489,8 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mediarepo-api" name = "mediarepo-api"
version = "0.25.0" version = "0.26.0"
source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=cd7bcc8d688d05275679d20af71231098602009b#cd7bcc8d688d05275679d20af71231098602009b" source = "git+https://github.com/Trivernis/mediarepo-api.git?rev=bd21f48f41aa2943f76b21addf137b2e58d492ca#bd21f48f41aa2943f76b21addf137b2e58d492ca"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bromine", "bromine",

@ -25,7 +25,7 @@ features = [ "env-filter" ]
[dependencies.mediarepo-api] [dependencies.mediarepo-api]
git = "https://github.com/Trivernis/mediarepo-api.git" git = "https://github.com/Trivernis/mediarepo-api.git"
rev = "cd7bcc8d688d05275679d20af71231098602009b" rev = "bd21f48f41aa2943f76b21addf137b2e58d492ca"
features = [ "tauri-plugin" ] features = [ "tauri-plugin" ]
[features] [features]

@ -25,12 +25,9 @@ import {
StartDaemonRequest, StartDaemonRequest,
UpdateFileNameRequest UpdateFileNameRequest
} from "./api-types/requests"; } from "./api-types/requests";
import { import {RepositoryData, RepositoryMetadata, SizeMetadata} from "./api-types/repo";
RepositoryData,
RepositoryMetadata,
SizeMetadata
} from "./api-types/repo";
import {NamespaceData, TagData} from "./api-types/tags"; import {NamespaceData, TagData} from "./api-types/tags";
import {ShortCache} from "./ShortCache";
export class MediarepoApi { export class MediarepoApi {
@ -99,7 +96,7 @@ export class MediarepoApi {
} }
public static async findFiles(request: FindFilesRequest): Promise<FileBasicData[]> { public static async findFiles(request: FindFilesRequest): Promise<FileBasicData[]> {
return this.invokePlugin(ApiFunction.FindFiles, request); return ShortCache.cached(request, () => this.invokePlugin(ApiFunction.FindFiles, request), 5000, "findFiles");
} }
public static async getFileMetadata(request: GetFileMetadataRequest): Promise<FileMetadata> { public static async getFileMetadata(request: GetFileMetadataRequest): Promise<FileMetadata> {
@ -123,7 +120,7 @@ export class MediarepoApi {
} }
public static async getAllTags(): Promise<TagData[]> { public static async getAllTags(): Promise<TagData[]> {
return this.invokePlugin(ApiFunction.GetAllTags); return ShortCache.cached("all-tags", () => this.invokePlugin(ApiFunction.GetAllTags), 2000);
} }
public static async getAllNamespaces(): Promise<NamespaceData[]> { public static async getAllNamespaces(): Promise<NamespaceData[]> {
@ -131,7 +128,12 @@ export class MediarepoApi {
} }
public static async getTagsForFiles(request: GetTagsForFilesRequest): Promise<TagData[]> { public static async getTagsForFiles(request: GetTagsForFilesRequest): Promise<TagData[]> {
return this.invokePlugin(ApiFunction.GetTagsForFiles, request); return ShortCache.cached(
request,
() => this.invokePlugin(ApiFunction.GetTagsForFiles, request),
1000,
"getTagsForFiles"
);
} }
public static async createTags(request: CreateTagsRequest): Promise<TagData[]> { public static async createTags(request: CreateTagsRequest): Promise<TagData[]> {
@ -151,7 +153,7 @@ export class MediarepoApi {
} }
public static async getFrontendState(): Promise<string> { public static async getFrontendState(): Promise<string> {
return this.invokePlugin(ApiFunction.GetFrontendState); return ShortCache.cached("frontend-state", () => this.invokePlugin(ApiFunction.GetFrontendState), 1000);
} }
public static async setFrontendState(request: SetFrontendStateRequest): Promise<void> { public static async setFrontendState(request: SetFrontendStateRequest): Promise<void> {

@ -0,0 +1,69 @@
type CacheEntry<T> = {
ttl: number,
value: T,
}
const cacheMap: {
[key: string]: CacheEntry<any>
} = {};
export class ShortCache {
public static async cached<T>(
key: any,
producer: () => Promise<T>,
ttl: number = 1000,
prefix: string = ""
): Promise<T> {
const cacheKey = prefix + JSON.stringify(key);
const entry = this.getCacheEntry<T>(cacheKey, ttl);
if (entry) {
console.debug("cache hit for key", cacheKey);
return entry;
} else {
console.debug("cache miss key", cacheKey);
const value = await producer();
this.addCacheEntry(cacheKey, value, ttl);
return value;
}
}
public static startTicking() {
(async () => {
while (true) {
ShortCache.tick();
await new Promise(resolve => setTimeout(resolve, 100));
}
})();
}
private static addCacheEntry(key: string, value: any, ttl: number) {
cacheMap[key] = {
ttl,
value
};
console.debug("added cache entry with key", key);
}
private static getCacheEntry<T>(key: string, ttl: number): T | undefined {
const entry = cacheMap[key];
if (entry) {
entry.ttl = ttl;
}
return entry?.value;
}
private static tick() {
for (let key in cacheMap) {
cacheMap[key].ttl -= 100;
if (cacheMap[key].ttl < 0) {
console.debug("purged cache entry with key", key);
delete cacheMap[key];
}
}
}
}
ShortCache.startTicking();

@ -1,12 +1,56 @@
export type FilterExpression = export type FilterExpression = FilterExpressionOrExpression | FilterExpressionQuery;
{ OrExpression: TagQuery[] }
| { Query: TagQuery }; export type FilterExpressionOrExpression = {
OrExpression: FilterQuery[],
};
export type FilterExpressionQuery = {
Query: FilterQuery;
};
export type FilterQuery = FilterQueryTag | FilterQueryProperty;
export type FilterQueryTag = { Tag: TagQuery };
export type FilterQueryProperty = { Property: PropertyQuery };
export type TagQuery = { export type TagQuery = {
negate: boolean, negate: boolean,
tag: string, tag: string,
}; };
export type PropertyKeys =
"Status"
| "FileSize"
| "ImportedTime"
| "ChangedTime"
| "CreatedTime"
| "TagCount"
| "Cd"
| "Id";
export type PropertyQuery = PropertyQueryStatus
| PropertyQueryFileSize
| PropertyQueryImportedTime
| PropertyQueryChangedTime
| PropertyQueryCreatedTime
| PropertyQueryTagCount
| PropertyQueryCd
| PropertyQueryId;
export type PropertyQueryStatus = { Status: FileStatus };
export type PropertyQueryFileSize = { FileSize: ValueComparator<number> };
export type PropertyQueryImportedTime = { ImportedTime: ValueComparator<Date> };
export type PropertyQueryChangedTime = { ChangedTime: ValueComparator<Date> };
export type PropertyQueryCreatedTime = { CreatedTime: ValueComparator<Date> };
export type PropertyQueryTagCount = { TagCount: ValueComparator<number> };
export type PropertyQueryCd = { Cd: string };
export type PropertyQueryId = { Id: number };
export type ValueComparator<T> =
{ Less: T }
| { Equal: T }
| { Greater: T }
| { Between: T[] }
export type SortKey = { Namespace: SortNamespace } export type SortKey = { Namespace: SortNamespace }
| { FileName: SortDirection } | { FileName: SortDirection }
| { FileSize: SortDirection } | { FileSize: SortDirection }

@ -0,0 +1,292 @@
import {FileStatus, FilterQuery, PropertyQuery, ValueComparator} from "../api-types/files";
export type Comparator = "Less" | "Equal" | "Greater" | "Between";
export type PropertyType =
"Status"
| "FileSize"
| "ImportedTime"
| "ChangedTime"
| "CreatedTime"
| "TagCount"
| "Cd"
| "Id";
export class FilterQueryBuilder {
public static tag(tag: string, negate: boolean): FilterQuery {
return { Tag: { tag, negate } };
}
public static status(status: FileStatus): FilterQuery {
return filterQuery({ Status: status });
}
public static fileSize(size: number, comparator: Comparator, max_size?: number): FilterQuery {
return filterQuery(
{ FileSize: valuesToCompareEnum(size, comparator, max_size) });
}
public static importedTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
return filterQuery({
ImportedTime: valuesToCompareEnum(date, comparator,
max_date
)
});
}
public static changedTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
return filterQuery({
ChangedTime: valuesToCompareEnum(date, comparator, max_date)
});
}
public static createdTime(date: Date, comparator: Comparator, max_date: Date): FilterQuery {
return filterQuery({
CreatedTime: valuesToCompareEnum(date, comparator, max_date)
});
}
public static tagCount(count: number, comparator: Comparator, max_count: number): FilterQuery {
return filterQuery({
TagCount: valuesToCompareEnum(count, comparator, max_count)
});
}
public static contentDescriptor(descriptor: string): FilterQuery {
return filterQuery({ Cd: descriptor });
}
public static fileId(id: number): FilterQuery {
return filterQuery({ Id: id });
}
public static buildFilterFromString(filterStr: string): FilterQuery {
filterStr = filterStr.trim();
if (filterStr.startsWith(".")) {
const cleanFilter = filterStr.replace(/^\./, "");
const parsedPropertyFilter = this.parsePropertyFilterQuery(cleanFilter);
if (parsedPropertyFilter) {
return parsedPropertyFilter;
}
} else if (filterStr.startsWith("-")) {
const tag = filterStr.replace(/^-/, "").trim();
return this.tag(tag, true);
}
return this.tag(filterStr, false);
}
private static parsePropertyFilterQuery(expression: string): FilterQuery | undefined {
let propertyName = "";
let compareValue = "";
let rawComparator = "";
let comparatorStarted = false;
let valueStarted = false;
for (const char of expression) {
console.log(char);
if (!valueStarted) {
switch (char) {
case " ":
break;
case "=":
case "!":
case ">":
case "<":
rawComparator += char;
comparatorStarted = true;
break;
default:
valueStarted = comparatorStarted;
if (valueStarted) {
compareValue += char;
} else {
propertyName += char;
}
}
} else {
compareValue += char;
}
}
return this.parseQueryFromParts(propertyName, rawComparator, compareValue);
}
private static parseQueryFromParts(
propertyName: string,
rawComparator: string,
compareValue: string
): FilterQuery | undefined {
const property = this.parsePropertyName(propertyName);
const comparator = this.parseComparator(rawComparator);
console.log("Parts: ", propertyName, rawComparator, compareValue);
if (property && comparator) {
let value;
switch (property) {
case "Status":
value = parseStatus(compareValue);
if (comparator === "Equal" && value != undefined) {
return this.status(value);
}
break;
case "FileSize":
value = this.parsePropertyValue(compareValue, parseNumber);
if (value != undefined) {
return this.fileSize(value[0], comparator, value[1]);
}
break;
case "ImportedTime":
value = this.parsePropertyValue(compareValue, parseDate);
if (value != undefined) {
return this.importedTime(value[0], comparator, value[1]);
}
break;
case "ChangedTime":
value = this.parsePropertyValue(compareValue, parseDate);
if (value != undefined) {
return this.changedTime(value[0], comparator, value[1]);
}
break;
case "CreatedTime":
value = this.parsePropertyValue(compareValue, parseDate);
if (value != undefined) {
return this.createdTime(value[0], comparator, value[1]);
}
break;
case "TagCount":
value = this.parsePropertyValue(compareValue, parseNumber);
if (value != undefined) {
return this.tagCount(value[0], comparator, value[1]);
}
break;
case "Cd":
if (compareValue) {
return this.contentDescriptor(compareValue);
}
break;
case "Id":
value = parseNumber(compareValue);
if (value != undefined) {
return this.fileId(value);
}
break;
}
}
return undefined;
}
private static parseComparator(comparatorStr: string): Comparator | undefined {
switch (comparatorStr) {
case "=":
case "==":
return "Equal";
case "<":
return "Less";
case ">":
return "Greater";
default:
return;
}
}
private static parsePropertyName(nameStr: string): PropertyType | undefined {
switch (nameStr.toLowerCase().replace(/-_/g, "")) {
case "status":
return "Status";
case "filesize":
return "FileSize";
case "importedat":
case "importeddate":
case "importedtime":
return "ImportedTime";
case "changedat":
case "changeddate":
case "changedtime":
return "ChangedTime";
case "createdat":
case "createddate":
case "createdtime":
return "CreatedTime";
case "tagcount":
return "TagCount";
case "cd":
case "contentdescriptor":
return "Cd";
case "id":
case "fileid":
return "Id";
default:
return;
}
}
private static parsePropertyValue<T>(
valueStr: string,
parseFn: (valueStr: string) => T | undefined
): T[] | undefined {
const [firstValue, secondValue] = valueStr.split(" ");
if (secondValue != undefined) {
const firstValueParsed = parseFn(firstValue);
const secondValueParsed = parseFn(secondValue);
if (firstValueParsed && secondValueParsed) {
return [firstValueParsed, secondValueParsed];
}
} else {
const value = parseFn(firstValue);
return value != undefined ? [value] : undefined;
}
return;
}
}
function filterQuery(propertyQuery: PropertyQuery): FilterQuery {
return { Property: propertyQuery };
}
function valuesToCompareEnum<T>(min_value: T, comparator: Comparator, max_value?: T): ValueComparator<T> {
switch (comparator) {
case "Less":
return { Less: min_value };
case "Equal":
return { Equal: min_value };
case "Greater":
return { Greater: min_value };
case "Between":
return { Between: [min_value, max_value!] };
}
}
function parseNumber(value: string): number | undefined {
const num = Number(value);
return isNaN(num) ? undefined : num;
}
function parseDate(value: string): Date | undefined {
const date = Date.parse(value);
if (isNaN(date)) {
return undefined;
}
return new Date(date);
}
function parseStatus(value: string): FileStatus | undefined {
switch (value.toLowerCase()) {
case "imported":
return "Imported";
case "archived":
return "Archived";
case "deleted":
return "Deleted";
default:
return undefined;
}
}

@ -0,0 +1,75 @@
import {FilterExpression, FilterQuery} from "../api-types/files";
import * as deepEqual from "fast-deep-equal";
export class SearchFilters {
constructor(private filters: FilterExpression[]) {
}
public get length() {
return this.filters.length;
}
public getFilters(): FilterExpression[] {
return this.filters;
}
public hasFilter(expression: FilterExpression): boolean {
return !!this.filters.find(f => deepEqual(f, expression));
}
public addFilter(filter: FilterQuery, index: number) {
this.filters = [...this.filters.slice(
0,
index
), { Query: filter }, ...this.filters.slice(index)];
}
public appendFilter(filter: FilterQuery) {
this.filters.push({ Query: filter });
}
public removeFilter(filterToRemove: FilterExpression) {
this.filters = this.filters.filter(f => !deepEqual(f, filterToRemove));
}
public removeFilterAtIndex(index: number) {
this.filters.splice(index, 1);
}
public appendSubfilter(filter: FilterQuery, index: number) {
const expressionEntry = this.filters[index];
if (expressionEntry && "OrExpression" in expressionEntry) {
expressionEntry["OrExpression"]!.push(filter);
} else {
const otherQuery = expressionEntry["Query"]!;
let entry = expressionEntry as unknown as { OrExpression: FilterQuery[], Query: undefined };
entry["Query"] = undefined;
entry["OrExpression"] = [otherQuery, filter];
}
}
public removeSubfilter(queryToRemove: FilterQuery) {
let index = this.filters.findIndex(f => {
if ("Query" in f) {
return false;
} else {
f["OrExpression"] = f["OrExpression"]!.filter(q => !deepEqual(q, queryToRemove));
return (f["OrExpression"]!.length === 0);
}
});
this.filters.splice(index);
}
public removeSubfilterAtIndex(index: number, subindex: number) {
const filterEntry = this.filters[index];
if (filterEntry && "OrExpression" in filterEntry) {
filterEntry["OrExpression"]!.splice(subindex, 1);
if (filterEntry["OrExpression"]!.length === 0) {
this.removeFilterAtIndex(index);
}
}
}
}

@ -1,5 +1,5 @@
@use 'sass:map'; @use 'sass:map';
@use '~@angular/material' as mat; @use '@angular/material' as mat;
@mixin color($theme) { @mixin color($theme) {
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);

@ -1,22 +1,14 @@
import {AfterViewInit, Component, OnInit} from "@angular/core"; import {AfterViewInit, Component, OnInit} from "@angular/core";
import {Repository} from "../../../../api/models/Repository"; import {Repository} from "../../../../api/models/Repository";
import { import {RepositoryService} from "../../../services/repository/repository.service";
RepositoryService
} from "../../../services/repository/repository.service";
import {MatDialog, MatDialogRef} from "@angular/material/dialog"; import {MatDialog, MatDialogRef} from "@angular/material/dialog";
import { import {DownloadDaemonDialogComponent} from "./download-daemon-dialog/download-daemon-dialog.component";
DownloadDaemonDialogComponent
} from "./download-daemon-dialog/download-daemon-dialog.component";
import { import {
AddRepositoryDialogComponent AddRepositoryDialogComponent
} from "../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component"; } from "../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component";
import { import {ErrorBrokerService} from "../../../services/error-broker/error-broker.service";
ErrorBrokerService
} from "../../../services/error-broker/error-broker.service";
import {BehaviorSubject} from "rxjs"; import {BehaviorSubject} from "rxjs";
import { import {BusyDialogComponent} from "../../shared/app-common/busy-dialog/busy-dialog.component";
BusyDialogComponent
} from "../../shared/app-common/busy-dialog/busy-dialog.component";
import {JobService} from "../../../services/job/job.service"; import {JobService} from "../../../services/job/job.service";
import {StateService} from "../../../services/state/state.service"; import {StateService} from "../../../services/state/state.service";
@ -83,7 +75,7 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
await this.repoService.loadRepositories(); await this.repoService.loadRepositories();
await this.stateService.loadState(); await this.stateService.loadState();
dialogContext.dialog.close(true); dialogContext.dialog.close(true);
} catch (err) { } catch (err: any) {
this.errorBroker.showError(err); this.errorBroker.showError(err);
dialogContext.message.next( dialogContext.message.next(
"Failed to open repository: " + err.toString()); "Failed to open repository: " + err.toString());
@ -92,6 +84,22 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
} }
} }
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
}
public async onOpenRepository(repository: Repository) {
if (!repository.local) {
await this.selectRepository(repository);
} else {
await this.startDaemonAndSelectRepository(repository);
}
}
private async forceCloseRepository() { private async forceCloseRepository() {
try { try {
await this.repoService.closeSelectedRepository(); await this.repoService.closeSelectedRepository();
@ -132,23 +140,17 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
} }
}); });
return {message: dialogMessage, dialog}; return { message: dialogMessage, dialog };
}
public openAddRepositoryDialog() {
this.dialog.open(AddRepositoryDialogComponent, {
disableClose: true,
minWidth: "30%",
minHeight: "30%",
});
} }
private async checkAndPromptDaemonExecutable() { private async checkAndPromptDaemonExecutable() {
if (!await this.repoService.checkDameonConfigured()) { if (!await this.repoService.checkDameonConfigured()) {
const result = await this.dialog.open(DownloadDaemonDialogComponent, const result = await this.dialog.open(
DownloadDaemonDialogComponent,
{ {
disableClose: true, disableClose: true,
}).afterClosed().toPromise(); }
).afterClosed().toPromise();
if (result) { if (result) {
// recursion avoidance // recursion avoidance
setTimeout( setTimeout(
@ -156,12 +158,4 @@ export class RepositoriesTabComponent implements OnInit, AfterViewInit {
} }
} }
} }
public async onOpenRepository(repository: Repository) {
if (!repository.local) {
await this.selectRepository(repository);
} else {
await this.startDaemonAndSelectRepository(repository);
}
}
} }

@ -1,10 +1,6 @@
import {NgModule} from "@angular/core"; import {NgModule} from "@angular/core";
import { import {ConfirmDialogComponent} from "./confirm-dialog/confirm-dialog.component";
ConfirmDialogComponent import {BusyIndicatorComponent} from "./busy-indicator/busy-indicator.component";
} from "./confirm-dialog/confirm-dialog.component";
import {
BusyIndicatorComponent
} from "./busy-indicator/busy-indicator.component";
import {ContextMenuComponent} from "./context-menu/context-menu.component"; import {ContextMenuComponent} from "./context-menu/context-menu.component";
import {CommonModule} from "@angular/common"; import {CommonModule} from "@angular/common";
import {NgIconsModule} from "@ng-icons/core"; import {NgIconsModule} from "@ng-icons/core";
@ -12,16 +8,10 @@ import {MatProgressSpinnerModule} from "@angular/material/progress-spinner";
import {MatButtonModule} from "@angular/material/button"; import {MatButtonModule} from "@angular/material/button";
import {MatDialogModule} from "@angular/material/dialog"; import {MatDialogModule} from "@angular/material/dialog";
import {MatMenuModule} from "@angular/material/menu"; import {MatMenuModule} from "@angular/material/menu";
import { import {ContentAwareImageComponent} from "./content-aware-image/content-aware-image.component";
ContentAwareImageComponent import {InputReceiverDirective} from "./input-receiver/input-receiver.directive";
} from "./content-aware-image/content-aware-image.component"; import {MetadataEntryComponent} from "./metadata-entry/metadata-entry.component";
import { import {BusyDialogComponent} from "./busy-dialog/busy-dialog.component";
InputReceiverDirective
} from "./input-receiver/input-receiver.directive";
import {
MetadataEntryComponent
} from "./metadata-entry/metadata-entry.component";
import { BusyDialogComponent } from './busy-dialog/busy-dialog.component';
@NgModule({ @NgModule({

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

@ -11,7 +11,7 @@ export class ContextMenuComponent {
public x: string = "0"; public x: string = "0";
public y: string = "0"; public y: string = "0";
@ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger @ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger;
constructor() { constructor() {
} }

@ -37,7 +37,7 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
partitionedGridEntries: Selectable<File>[][] = []; partitionedGridEntries: Selectable<File>[][] = [];
private shiftClicked = false; private shiftClicked = false;
private ctrlClicked = false; private ctrlClicked = false;
private gridEntries: Selectable<File>[] = [] private gridEntries: Selectable<File>[] = [];
constructor( constructor(
private tabService: TabService, private tabService: TabService,
@ -102,6 +102,55 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
await this.fileService.deleteThumbnails(file); await this.fileService.deleteThumbnails(file);
} }
public focus() {
this.inner.nativeElement.focus();
}
public handleKeydownEvent(event: KeyboardEvent) {
this.shiftClicked ||= event.shiftKey;
this.ctrlClicked ||= event.ctrlKey;
switch (event.key) {
case "ArrowRight":
this.handleArrowSelect("right");
break;
case "ArrowLeft":
this.handleArrowSelect("left");
break;
case "ArrowDown":
this.handleArrowSelect("down");
break;
case "ArrowUp":
this.handleArrowSelect("up");
break;
case "PageDown":
this.pageDown();
break;
case "PageUp":
this.pageUp();
break;
case "a":
case "A":
if (this.shiftClicked && this.ctrlClicked) {
this.selectNone();
} else if (this.ctrlClicked) {
event.preventDefault();
this.selectAll();
}
break;
case "Enter":
if (this.selectedEntries.length === 1) {
this.fileOpenEvent.emit(this.selectedEntries[0].data);
}
break;
}
}
public handleKeyupEvent(event: KeyboardEvent) {
this.shiftClicked = event.shiftKey ? false : this.shiftClicked;
this.ctrlClicked = event.ctrlKey ? false : this.ctrlClicked;
}
private setPartitionedGridEntries() { private setPartitionedGridEntries() {
this.partitionedGridEntries = []; this.partitionedGridEntries = [];
let scrollToIndex = -1; let scrollToIndex = -1;
@ -109,8 +158,10 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
for (let i = 0; i < (Math.ceil( for (let i = 0; i < (Math.ceil(
this.gridEntries.length / this.columns)); i++) { this.gridEntries.length / this.columns)); i++) {
const entries = this.gridEntries.slice(i * this.columns, const entries = this.gridEntries.slice(
Math.min(this.gridEntries.length, (i + 1) * this.columns)); i * this.columns,
Math.min(this.gridEntries.length, (i + 1) * this.columns)
);
this.partitionedGridEntries.push(entries); this.partitionedGridEntries.push(entries);
const preselectedEntry = entries.find( const preselectedEntry = entries.find(
@ -195,7 +246,7 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
selectedIndex += this.columns; selectedIndex += this.columns;
break; break;
case "left": case "left":
selectedIndex --; selectedIndex--;
break; break;
case "right": case "right":
selectedIndex++; selectedIndex++;
@ -221,61 +272,12 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit {
offsetTop = this.virtualScroll.measureScrollOffset("top"); offsetTop = this.virtualScroll.measureScrollOffset("top");
if (contentOffset < offsetTop + (viewportSize / 2)) { if (contentOffset < offsetTop + (viewportSize / 2)) {
this.virtualScroll.scrollToOffset((offsetTop + 130) - viewportSize/ 2); this.virtualScroll.scrollToOffset((offsetTop + 130) - viewportSize / 2);
} }
} }
} }
} }
public focus() {
this.inner.nativeElement.focus();
}
public handleKeydownEvent(event: KeyboardEvent) {
this.shiftClicked ||= event.shiftKey;
this.ctrlClicked ||= event.ctrlKey;
switch (event.key) {
case "ArrowRight":
this.handleArrowSelect("right");
break;
case "ArrowLeft":
this.handleArrowSelect("left");
break;
case "ArrowDown":
this.handleArrowSelect("down");
break;
case "ArrowUp":
this.handleArrowSelect("up");
break;
case "PageDown":
this.pageDown();
break;
case "PageUp":
this.pageUp();
break;
case "a":
case "A":
if (this.shiftClicked && this.ctrlClicked) {
this.selectNone();
} else if (this.ctrlClicked) {
event.preventDefault();
this.selectAll();
}
break;
case "Enter":
if (this.selectedEntries.length === 1) {
this.fileOpenEvent.emit(this.selectedEntries[0].data);
}
break;
}
}
public handleKeyupEvent(event: KeyboardEvent) {
this.shiftClicked = event.shiftKey? false : this.shiftClicked;
this.ctrlClicked = event.ctrlKey? false : this.ctrlClicked;
}
private pageDown() { private pageDown() {
if (this.virtualScroll) { if (this.virtualScroll) {
const offsetTop = this.virtualScroll.measureScrollOffset("top"); const offsetTop = this.virtualScroll.measureScrollOffset("top");

@ -1,10 +1,4 @@
import { import {AfterViewInit, Component, Input, OnChanges, SimpleChanges} from "@angular/core";
AfterViewInit,
Component,
Input,
OnChanges,
SimpleChanges
} from "@angular/core";
import {File} from "../../../../../api/models/File"; import {File} from "../../../../../api/models/File";
import {FileService} from "../../../../services/file/file.service"; import {FileService} from "../../../../services/file/file.service";
import {FileHelper} from "../../../../services/file/file.helper"; import {FileHelper} from "../../../../services/file/file.helper";
@ -21,9 +15,9 @@ export class FileThumbnailComponent implements OnChanges, AfterViewInit {
public thumbUrl: SafeResourceUrl | undefined; public thumbUrl: SafeResourceUrl | undefined;
private supportedThumbnailTypes = ["image", "video"] private supportedThumbnailTypes = ["image", "video"];
constructor( private fileService: FileService) { constructor(private fileService: FileService) {
} }
public async ngAfterViewInit() { public async ngAfterViewInit() {
@ -33,7 +27,8 @@ export class FileThumbnailComponent implements OnChanges, AfterViewInit {
public async ngOnChanges(changes: SimpleChanges) { public async ngOnChanges(changes: SimpleChanges) {
if (changes["file"]) { if (changes["file"]) {
this.thumbUrl = this.fileService.buildThumbnailUrl(this.file, this.thumbUrl = this.fileService.buildThumbnailUrl(this.file,
250, 250); 250, 250
);
} }
} }

@ -1,8 +1,6 @@
import {Component, EventEmitter, Output} from "@angular/core"; import {Component, EventEmitter, Output} from "@angular/core";
import {ImportService} from "../../../../../services/import/import.service"; import {ImportService} from "../../../../../services/import/import.service";
import { import {ErrorBrokerService} from "../../../../../services/error-broker/error-broker.service";
ErrorBrokerService
} from "../../../../../services/error-broker/error-broker.service";
import {AddFileOptions} from "../../../../../models/AddFileOptions"; import {AddFileOptions} from "../../../../../models/AddFileOptions";
import {File} from "../../../../../../api/models/File"; import {File} from "../../../../../../api/models/File";
import {DialogFilter} from "@tauri-apps/api/dialog"; import {DialogFilter} from "@tauri-apps/api/dialog";
@ -26,12 +24,12 @@ export class FilesystemImportComponent {
name: "Images", name: "Images",
extensions: ["png", "jpg", "jpeg", "webp", "bmp", "gif"] extensions: ["png", "jpg", "jpeg", "webp", "bmp", "gif"]
}, },
{name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"]}, { name: "Videos", extensions: ["mp4", "mkv", "wmv", "avi", "webm"] },
{name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"]}, { name: "Audio", extensions: ["mp3", "ogg", "wav", "flac", "aac"] },
{name: "Documents", extensions: ["pdf", "doc", "docx", "odf"]}, { name: "Documents", extensions: ["pdf", "doc", "docx", "odf"] },
{name: "Text", extensions: ["txt", "md"]}, { name: "Text", extensions: ["txt", "md"] },
{name: "All", extensions: ["*"]} { name: "All", extensions: ["*"] }
] ];
public resolving = false; public resolving = false;
@ -61,8 +59,10 @@ export class FilesystemImportComponent {
for (const file of this.files) { for (const file of this.files) {
try { try {
const resultFile = await this.importService.addLocalFile(file, const resultFile = await this.importService.addLocalFile(
this.importOptions); file,
this.importOptions
);
this.fileImported.emit(resultFile); this.fileImported.emit(resultFile);
} catch (err) { } catch (err) {
console.log(err); console.log(err);

@ -4,8 +4,11 @@
<div class="tag-input-list-and-actions"> <div class="tag-input-list-and-actions">
<div #tagInputList class="tag-input-list"> <div #tagInputList class="tag-input-list">
<div class="tag-input-list-inner"> <div class="tag-input-list-inner">
<div (click)="removeFilterExpression(filter)" *ngFor="let filter of filters" class="tag-input-item" <div (click)="removeFilterExpression(filter)" *ngFor="let filter of filters.getFilters()"
mat-ripple>{{filter.getDisplayName()}}</div> class="tag-input-item"
mat-ripple>
<app-filter-expression-item [filter]="filter"></app-filter-expression-item>
</div>
</div> </div>
</div> </div>
@ -14,7 +17,9 @@
</button> </button>
</div> </div>
<app-tag-input [allowWildcards]="true" (tagAdded)="addSearchTag($event); searchForFiles()" [allowNegation]="true" <app-tag-input (tagAdded)="addSearchQuery($event); searchForFiles()" [allowInvalid]="true"
[allowNegation]="true"
[allowWildcards]="true"
[availableTags]="getValidSearchTags()" [availableTags]="getValidSearchTags()"
class="full-width"> class="full-width">
<button (click)="openFilterDialog()" class="filter-dialog-button" mat-button> <button (click)="openFilterDialog()" class="filter-dialog-button" mat-button>
@ -33,9 +38,9 @@
<div class="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill> <div class="file-tag-list" fxFlex fxFlexAlign="start" fxFlexFill>
<app-busy-indicator [busy]="this.tagsLoading" [blurBackground]="true" [darkenBackground]="false"> <app-busy-indicator [blurBackground]="true" [busy]="this.tagsLoading" [darkenBackground]="false">
<cdk-virtual-scroll-viewport itemSize="50" maxBufferPx="4000" minBufferPx="500"> <cdk-virtual-scroll-viewport itemSize="50" maxBufferPx="4000" minBufferPx="500">
<div (click)="addSearchTag(tag.getNormalizedOutput())" <div (click)="addSearchQuery(tag.getNormalizedOutput())"
(contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)" (contextmenu)="contextMenuTag = tag; contextMenu.onContextMenu($event)"
*cdkVirtualFor="let tag of contextTags" class="selectable-tag"> *cdkVirtualFor="let tag of contextTags" class="selectable-tag">
<app-tag-item [tag]="tag"></app-tag-item> <app-tag-item [tag]="tag"></app-tag-item>

@ -1,28 +1,15 @@
import { import {AfterViewChecked, Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from "@angular/core";
AfterViewChecked,
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild
} from "@angular/core";
import {TagQuery} from "../../../../models/TagQuery";
import {SortKey} from "../../../../models/SortKey"; import {SortKey} from "../../../../models/SortKey";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {SortDialogComponent} from "./sort-dialog/sort-dialog.component"; import {SortDialogComponent} from "./sort-dialog/sort-dialog.component";
import { import {ErrorBrokerService} from "../../../../services/error-broker/error-broker.service";
ErrorBrokerService
} from "../../../../services/error-broker/error-broker.service";
import {
GenericFilter,
SingleFilterExpression
} from "../../../../models/GenericFilter";
import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component"; import {FilterDialogComponent} from "./filter-dialog/filter-dialog.component";
import {Tag} from "../../../../../api/models/Tag"; import {Tag} from "../../../../../api/models/Tag";
import {clipboard} from "@tauri-apps/api"; import {clipboard} from "@tauri-apps/api";
import {TabState} from "../../../../models/TabState"; import {TabState} from "../../../../models/TabState";
import {FilterQueryBuilder} from "../../../../../api/models/FilterQueryBuilder";
import {SearchFilters} from "../../../../../api/models/SearchFilters";
import {FilterExpression} from "../../../../../api/api-types/files";
@Component({ @Component({
@ -32,7 +19,7 @@ import {TabState} from "../../../../models/TabState";
}) })
export class FileSearchComponent implements AfterViewChecked, OnInit { export class FileSearchComponent implements AfterViewChecked, OnInit {
public sortExpression: SortKey[] = []; public sortExpression: SortKey[] = [];
public filters: GenericFilter[] = []; public filters: SearchFilters = new SearchFilters([]);
@Input() availableTags: Tag[] = []; @Input() availableTags: Tag[] = [];
@Input() contextTags: Tag[] = []; @Input() contextTags: Tag[] = [];
@ -73,40 +60,43 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
this.searchEndEvent.emit(); this.searchEndEvent.emit();
} }
public addSearchTag(tag: string) { public addSearchQuery(queryStr: string) {
this.filters.push(new SingleFilterExpression(TagQuery.fromString(tag))); let filter = FilterQueryBuilder.buildFilterFromString(queryStr);
tag = tag.replace(/^-/g, "");
if (this.filters.filter(t => t.partiallyEq(tag)).length > 1) { if (filter) {
const index = this.filters.findIndex(t => t.partiallyEq(tag)); this.filters.removeFilter({ Query: filter });
this.filters.splice(index, 1); this.filters.appendFilter(filter);
} }
this.state.setFilters(this.filters);
queryStr = queryStr.replace(/^-/g, "");
this.state.setTagFilters(this.filters);
} }
public getValidSearchTags(): Tag[] { public getValidSearchTags(): Tag[] {
return this.availableTags.filter(t => this.filters.findIndex( return this.availableTags.filter(t => !this.filters.hasFilter({
f => f.partiallyEq(t.getNormalizedOutput())) < 0); Query: FilterQueryBuilder.tag(
t.getNormalizedOutput(),
false
)
}));
} }
public async removeAllSearchTags() { public async removeAllSearchTags() {
this.filters = []; this.filters = new SearchFilters([]);
this.state.setFilters([]); this.state.setTagFilters(this.filters);
} }
public async removeFilterExpression(expr: GenericFilter) { public async removeFilterExpression(expr: FilterExpression) {
const index = this.filters.indexOf(expr); this.filters.removeFilter(expr);
if (index >= 0) { this.state.setTagFilters(this.filters);
this.filters.splice(index, 1);
}
this.state.setFilters(this.filters);
} }
public openSortDialog() { public openSortDialog() {
const sortEntries = this.sortExpression.map( const sortEntries = this.sortExpression.map(
key => JSON.parse(JSON.stringify(key))).map( key => JSON.parse(JSON.stringify(key))).map(
key => new SortKey(key.sortType, key.sortDirection, key => new SortKey(key.sortType, key.sortDirection,
key.namespaceName)); key.namespaceName
));
const openedDialog = this.dialog.open(SortDialogComponent, { const openedDialog = this.dialog.open(SortDialogComponent, {
minWidth: "40vw", minWidth: "40vw",
data: { data: {
@ -123,7 +113,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
} }
public openFilterDialog(): void { public openFilterDialog(): void {
const filterEntries = this.filters.map(f => f.clone()); const filterEntries = this.filters;
const filterDialog = this.dialog.open(FilterDialogComponent, { const filterDialog = this.dialog.open(FilterDialogComponent, {
minWidth: "25vw", minWidth: "25vw",
maxHeight: "80vh", maxHeight: "80vh",
@ -136,7 +126,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
filterDialog.afterClosed().subscribe(async (filterExpression) => { filterDialog.afterClosed().subscribe(async (filterExpression) => {
if (filterExpression !== undefined || filterExpression?.length > 0) { if (filterExpression !== undefined || filterExpression?.length > 0) {
this.filters = filterExpression; this.filters = filterExpression;
this.state.setFilters(this.filters); this.state.setTagFilters(this.filters);
} }
}); });
} }

@ -0,0 +1,14 @@
<span *ngIf="is('OrExpression')" class="or-expression">
<ng-container *ngFor="let query of this.orExpression().OrExpression">
<app-property-query-item *ngIf="this.queryIs(query, 'Property')"
[propertyQuery]="this.propertyQuery(query).Property"></app-property-query-item>
<app-tag-query-item *ngIf="this.queryIs(query, 'Tag')"
[tagQuery]="this.tagQuery(query).Tag"></app-tag-query-item>
</ng-container>
</span>
<span *ngIf="is('Query')" class="query">
<app-property-query-item *ngIf="this.queryIs(this.query().Query, 'Property')"
[propertyQuery]="this.propertyQuery(this.query().Query).Property"></app-property-query-item>
<app-tag-query-item *ngIf="this.queryIs(this.query().Query, 'Tag')"
[tagQuery]="this.tagQuery(this.query().Query).Tag"></app-tag-query-item>
</span>

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

@ -0,0 +1,47 @@
import {Component, Input} from "@angular/core";
import {
FilterExpression,
FilterExpressionOrExpression,
FilterExpressionQuery,
FilterQuery,
FilterQueryProperty,
FilterQueryTag
} from "../../../../../../api/api-types/files";
@Component({
selector: "app-filter-expression-item",
templateUrl: "./filter-expression-item.component.html",
styleUrls: ["./filter-expression-item.component.scss"]
})
export class FilterExpressionItemComponent {
@Input() filter!: FilterExpression;
constructor() {
}
public is(key: "OrExpression" | "Query"): boolean {
return key in this.filter;
}
public orExpression(): FilterExpressionOrExpression {
return this.filter as FilterExpressionOrExpression;
}
public query(): FilterExpressionQuery {
return this.filter as FilterExpressionQuery;
}
public queryIs(query: FilterQuery, key: "Property" | "Tag"): boolean {
return key in query;
}
public propertyQuery(query: FilterQuery): FilterQueryProperty {
return query as FilterQueryProperty;
}
public tagQuery(query: FilterQuery): FilterQueryTag {
return query as FilterQueryTag;
}
}

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

@ -0,0 +1,101 @@
import {Component, Input, OnChanges, OnInit, SimpleChanges} from "@angular/core";
import {PropertyQuery, ValueComparator} from "../../../../../../../api/api-types/files";
@Component({
selector: "app-property-query-item",
templateUrl: "./property-query-item.component.html",
styleUrls: ["./property-query-item.component.scss"]
})
export class PropertyQueryItemComponent implements OnInit, OnChanges {
@Input() propertyQuery!: PropertyQuery;
public stringExpression: string = "No Expression";
constructor() {
}
private static buildExpression(property: string, comparator: string, value: string): string {
return `.${property} ${comparator} ${value}`;
}
public ngOnInit(): void {
this.stringExpression = this.getStringExpression();
}
public ngOnChanges(changes: SimpleChanges): void {
if (changes["propertyQuery"]) {
this.stringExpression = this.getStringExpression();
}
}
public getStringExpression(): string {
if ("Status" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression("Status", "is", this.propertyQuery.Status);
} else if ("FileSize" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"FileSize",
this.getComparator(this.propertyQuery.FileSize),
this.getValue(this.propertyQuery.FileSize).toString()
);
} else if ("ImportedTime" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"ImportedTime",
this.getComparator(this.propertyQuery.ImportedTime),
this.getValue(this.propertyQuery.ImportedTime).toISOString()
);
} else if ("ChangedTime" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"ChangedTime",
this.getComparator(this.propertyQuery.ChangedTime),
this.getValue(this.propertyQuery.ChangedTime).toISOString()
);
} else if ("CreatedTime" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"CreatedTime",
this.getComparator(this.propertyQuery.CreatedTime),
this.getValue(this.propertyQuery.CreatedTime).toISOString()
);
} else if ("TagCount" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression(
"TagCount",
this.getComparator(this.propertyQuery.TagCount),
this.getValue(this.propertyQuery.TagCount).toString()
);
} else if ("Cd" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression("ContentDescriptor", "is", this.propertyQuery.Cd);
} else if ("Id" in this.propertyQuery) {
return PropertyQueryItemComponent.buildExpression("FileId", "is", this.propertyQuery.Id.toString());
} else {
return "Invalid Expression";
}
}
public getComparator(value: ValueComparator<any>): "=" | "<" | ">" | "between" {
if ("Greater" in value) {
return ">";
} else if ("Equal" in value) {
return "=";
} else if ("Less" in value) {
return "<";
} else {
return "between";
}
}
public getValue<T>(value: ValueComparator<T>): T {
const singleValueKeys: ("Greater" | "Equal" | "Less")[] = ["Greater", "Equal", "Less"];
for (const key of singleValueKeys) {
if (key in value) {
//@ts-ignore
return value[key];
}
}
if ("Between" in value) {
return value.Between[0];
} else {
return "" as unknown as T; // unreachable
}
}
}

@ -0,0 +1 @@
<span><span *ngIf="tagQuery.negate" class="tag-negation">-</span>{{tagQuery.tag}}</span>

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

@ -0,0 +1,15 @@
import {Component, Input} from "@angular/core";
import {TagQuery} from "../../../../../../../api/api-types/files";
@Component({
selector: "app-tag-query-item",
templateUrl: "./tag-query-item.component.html",
styleUrls: ["./tag-query-item.component.scss"]
})
export class TagQueryItemComponent {
@Input() tagQuery!: TagQuery;
constructor() {
}
}

@ -12,7 +12,7 @@ import {TagService} from "../../../../../services/tag/tag.service";
}) })
export class SortDialogComponent { export class SortDialogComponent {
public sortEntries: SortKey[] = [] public sortEntries: SortKey[] = [];
public suggestedNamespaces: Namespace[] = []; public suggestedNamespaces: Namespace[] = [];
private namespaces: Namespace[] = []; private namespaces: Namespace[] = [];
@ -24,6 +24,20 @@ export class SortDialogComponent {
namespaces => this.namespaces = namespaces); namespaces => this.namespaces = namespaces);
} }
private static compareSuggestionNamespaces(query: string, l: string, r: string): number {
if (l.startsWith(query) && !r.startsWith(query)) {
return -1;
} else if (!l.startsWith(query) && r.startsWith(query)) {
return 1;
} else if (l.length < r.length) {
return -1;
} else if (l.length > r.length) {
return 1;
} else {
return l.localeCompare(r);
}
}
addNewSortKey() { addNewSortKey() {
const sortKey = new SortKey("FileName", "Ascending", undefined); const sortKey = new SortKey("FileName", "Ascending", undefined);
this.sortEntries.push(sortKey); this.sortEntries.push(sortKey);
@ -44,7 +58,8 @@ export class SortDialogComponent {
public onSortEntryDrop(event: CdkDragDrop<SortKey[]>): void { public onSortEntryDrop(event: CdkDragDrop<SortKey[]>): void {
moveItemInArray(this.sortEntries, event.previousIndex, moveItemInArray(this.sortEntries, event.previousIndex,
event.currentIndex); event.currentIndex
);
} }
public updateAutocompleteSuggestions(value: string): void { public updateAutocompleteSuggestions(value: string): void {
@ -52,18 +67,4 @@ export class SortDialogComponent {
(a, b) => SortDialogComponent.compareSuggestionNamespaces(value, a.name, b.name)) (a, b) => SortDialogComponent.compareSuggestionNamespaces(value, a.name, b.name))
.slice(0, 50); .slice(0, 50);
} }
private static compareSuggestionNamespaces(query: string, l: string, r: string): number {
if (l.startsWith(query) && !r.startsWith(query)) {
return -1;
} else if (!l.startsWith(query) && r.startsWith(query)) {
return 1;
} else if (l.length < r.length) {
return -1;
} else if (l.length > r.length) {
return 1;
} else {
return l.localeCompare(r);
}
}
} }

@ -23,21 +23,15 @@ import {MatInputModule} from "@angular/material/input";
import { import {
TagFilterListItemComponent TagFilterListItemComponent
} from "./file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component"; } from "./file-search/filter-dialog/tag-filter-list-item/tag-filter-list-item.component";
import { import {SortDialogComponent} from "./file-search/sort-dialog/sort-dialog.component";
SortDialogComponent import {FilterDialogComponent} from "./file-search/filter-dialog/filter-dialog.component";
} from "./file-search/sort-dialog/sort-dialog.component";
import {
FilterDialogComponent
} from "./file-search/filter-dialog/filter-dialog.component";
import {MatListModule} from "@angular/material/list"; import {MatListModule} from "@angular/material/list";
import {MatDialogModule} from "@angular/material/dialog"; import {MatDialogModule} from "@angular/material/dialog";
import {AppCommonModule} from "../app-common/app-common.module"; import {AppCommonModule} from "../app-common/app-common.module";
import {DragDropModule} from "@angular/cdk/drag-drop"; import {DragDropModule} from "@angular/cdk/drag-drop";
import {TagModule} from "../tag/tag.module"; import {TagModule} from "../tag/tag.module";
import {FileImportComponent} from "./file-import/file-import.component"; import {FileImportComponent} from "./file-import/file-import.component";
import { import {FilesystemImportComponent} from "./file-import/filesystem-import/filesystem-import.component";
FilesystemImportComponent
} from "./file-import/filesystem-import/filesystem-import.component";
import {MatCheckboxModule} from "@angular/material/checkbox"; import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatProgressBarModule} from "@angular/material/progress-bar"; import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatMenuModule} from "@angular/material/menu"; import {MatMenuModule} from "@angular/material/menu";
@ -47,6 +41,11 @@ import {
} from "./file-metadata/editable-metadata-entry/editable-metadata-entry.component"; } from "./file-metadata/editable-metadata-entry/editable-metadata-entry.component";
import {ReactiveFormsModule} from "@angular/forms"; import {ReactiveFormsModule} from "@angular/forms";
import {MatAutocompleteModule} from "@angular/material/autocomplete"; import {MatAutocompleteModule} from "@angular/material/autocomplete";
import {FilterExpressionItemComponent} from "./file-search/filter-expression-item/filter-expression-item.component";
import {TagQueryItemComponent} from "./file-search/filter-expression-item/tag-query-item/tag-query-item.component";
import {
PropertyQueryItemComponent
} from "./file-search/filter-expression-item/property-query-item/property-query-item.component";
@NgModule({ @NgModule({
@ -60,6 +59,9 @@ import {MatAutocompleteModule} from "@angular/material/autocomplete";
FilesystemImportComponent, FilesystemImportComponent,
FileMetadataComponent, FileMetadataComponent,
EditableMetadataEntryComponent, EditableMetadataEntryComponent,
FilterExpressionItemComponent,
TagQueryItemComponent,
PropertyQueryItemComponent,
], ],
exports: [ exports: [
TagEditComponent, TagEditComponent,

@ -5,30 +5,16 @@ import {TabCategory} from "./TabCategory";
export class AppState { export class AppState {
private tabIdCounter = 0;
public tabs = new BehaviorSubject<TabState[]>([]); public tabs = new BehaviorSubject<TabState[]>([]);
public selectedTab = new BehaviorSubject<number | undefined>(undefined); public selectedTab = new BehaviorSubject<number | undefined>(undefined);
public repoName: string | undefined; public repoName: string | undefined;
private tabIdCounter = 0;
private readonly fileService: FileService private readonly fileService: FileService;
constructor(fileService: FileService) { constructor(fileService: FileService) {
this.fileService = fileService; this.fileService = fileService;
} }
public addTab(category: TabCategory): TabState {
const state = new TabState(this.tabIdCounter++, category, this.fileService);
this.tabs.next([...this.tabs.value, state]);
return state;
}
public async closeTab(uuid: number) {
const index = this.tabs.value.findIndex(t => t.uuid === uuid);
const tabs = this.tabs.value;
tabs.splice(index, 1);
this.tabs.next(tabs);
}
public static deserializeJson(stateString: string, fileService: FileService): AppState { public static deserializeJson(stateString: string, fileService: FileService): AppState {
let state = JSON.parse(stateString); let state = JSON.parse(stateString);
let appState = new AppState(fileService); let appState = new AppState(fileService);
@ -42,6 +28,19 @@ export class AppState {
return appState; return appState;
} }
public addTab(category: TabCategory): TabState {
const state = new TabState(this.tabIdCounter++, category, this.fileService);
this.tabs.next([...this.tabs.value, state]);
return state;
}
public async closeTab(uuid: number) {
const index = this.tabs.value.findIndex(t => t.uuid === uuid);
const tabs = this.tabs.value;
tabs.splice(index, 1);
this.tabs.next(tabs);
}
public serializeJson(): string { public serializeJson(): string {
const tabDTOs = this.tabs.value.map(tab => tab.getDTO()); const tabDTOs = this.tabs.value.map(tab => tab.getDTO());
return JSON.stringify({ return JSON.stringify({

@ -66,7 +66,7 @@ export class OrFilterExpression implements GenericFilter {
} }
public toBackendType(): FilterExpression { public toBackendType(): FilterExpression {
return createRustEnum(this.filter_type, this.filter) as unknown as FilterExpression; return createRustEnum(this.filter_type, createRustEnum("Tag", this.filter)) as unknown as FilterExpression;
} }
} }
@ -100,6 +100,6 @@ export class SingleFilterExpression implements GenericFilter {
} }
public toBackendType(): FilterExpression { public toBackendType(): FilterExpression {
return createRustEnum(this.filter_type, this.filter) as unknown as FilterExpression; return createRustEnum(this.filter_type, createRustEnum("Tag", this.filter)) as unknown as FilterExpression;
} }
} }

@ -2,15 +2,10 @@ import {BehaviorSubject} from "rxjs";
import {TabCategory} from "./TabCategory"; import {TabCategory} from "./TabCategory";
import {FileService} from "../services/file/file.service"; import {FileService} from "../services/file/file.service";
import {File} from "../../api/models/File"; import {File} from "../../api/models/File";
import {
GenericFilter,
OrFilterExpression,
SingleFilterExpression
} from "./GenericFilter";
import {SortKey} from "./SortKey"; import {SortKey} from "./SortKey";
import {TagQuery} from "./TagQuery";
import {debounceTime} from "rxjs/operators"; import {debounceTime} from "rxjs/operators";
import {mapNew} from "../../api/models/adaptors"; import {mapNew} from "../../api/models/adaptors";
import {SearchFilters} from "../../api/models/SearchFilters";
export class TabState { export class TabState {
public uuid: number; public uuid: number;
@ -20,14 +15,21 @@ export class TabState {
public loading = new BehaviorSubject<boolean>(false); public loading = new BehaviorSubject<boolean>(false);
public files = new BehaviorSubject<File[]>([]); public files = new BehaviorSubject<File[]>([]);
public filters = new BehaviorSubject<GenericFilter[]>([]); public filters = new BehaviorSubject<SearchFilters>(new SearchFilters([]));
public sortKeys = new BehaviorSubject<SortKey[]>( public sortKeys = new BehaviorSubject<SortKey[]>(
[new SortKey("FileImportedTime", [new SortKey(
"Ascending", undefined)]); "FileImportedTime",
"Ascending",
undefined
)]);
private fileService: FileService; private fileService: FileService;
constructor(uuid: number, category: TabCategory, fileService: FileService) { constructor(
uuid: number,
category: TabCategory,
fileService: FileService
) {
this.category = category; this.category = category;
this.uuid = uuid; this.uuid = uuid;
this.fileService = fileService; this.fileService = fileService;
@ -39,15 +41,43 @@ export class TabState {
} }
} }
public static fromDTO(
dto: any,
fileService: FileService
): TabState {
const state = new TabState(
dto.uuid,
dto.category,
fileService
);
const sortKeys = dto.sortKeys.map(
(s: { sortType: any, sortDirection: any, namespaceName: any }) =>
new SortKey(
s.sortType,
s.sortDirection,
s.namespaceName
)
);
state.filters.next(new SearchFilters(dto.filters ?? []));
state.sortKeys.next(sortKeys);
state.mode.next(dto.mode ?? "grid");
state.selectedCD.next(dto.selectedFileHash);
state.files.next((dto.files ?? []).map(mapNew(File)));
return state;
}
public async findFiles() { public async findFiles() {
this.loading.next(true); this.loading.next(true);
const files = await this.fileService.findFiles(this.filters.value, const files = await this.fileService.findFiles(
this.sortKeys.value); this.filters.value,
this.sortKeys.value
);
this.files.next(files); this.files.next(files);
this.loading.next(false); this.loading.next(false);
} }
public setFilters(filters: GenericFilter[]) { public setTagFilters(filters: SearchFilters) {
this.filters.next(filters); this.filters.next(filters);
} }
@ -55,36 +85,16 @@ export class TabState {
this.sortKeys.next(keys); this.sortKeys.next(keys);
} }
public static fromDTO(dto: any, fileService: FileService): TabState {
const state = new TabState(dto.uuid, dto.category, fileService);
const filters = dto.filters.map((f: {filter: any, filter_type: any}) => {
if (f.filter_type === "OrExpression") {
return new OrFilterExpression(f.filter.map((f: any) => new TagQuery(f.tag, f.negate)));
} else {
return new SingleFilterExpression(new TagQuery(f.filter.tag, f.filter.negate));
}
});
const sortKeys = dto.sortKeys.map((s: {sortType: any, sortDirection: any, namespaceName: any}) =>
new SortKey(s.sortType, s.sortDirection, s.namespaceName)
);
state.filters.next(filters);
state.sortKeys.next(sortKeys);
state.mode.next(dto.mode ?? "grid");
state.selectedCD.next(dto.selectedFileHash);
state.files.next((dto.files ?? []).map(mapNew(File)));
return state;
}
public getDTO(): any { public getDTO(): any {
return { return {
uuid: this.uuid, uuid: this.uuid,
category: this.category, category: this.category,
filters: this.filters.value, filters: this.filters.value.getFilters(),
sortKeys: this.sortKeys.value, sortKeys: this.sortKeys.value,
mode: this.mode.value, mode: this.mode.value,
selectedFileHash: this.selectedCD.value, selectedFileHash: this.selectedCD.value,
files: this.category === TabCategory.Import? this.files.value.map(f => f.rawData) : [], files: this.category === TabCategory.Import ? this.files.value.map(
f => f.rawData) : [],
}; };
} }
} }

@ -2,9 +2,9 @@ export type RustEnum<VariantData> = {
[key: string]: VariantData [key: string]: VariantData
}; };
export function createRustEnum<VariantData>(variant: string, data: VariantData): RustEnum<VariantData> { export function createRustEnum<T, VariantData>(variant: string, data: VariantData): T {
let enumInstance: RustEnum<VariantData> = {}; let enumInstance: RustEnum<VariantData> = {};
enumInstance[variant] = data; enumInstance[variant] = data;
return enumInstance; return enumInstance as unknown as T;
} }

@ -31,13 +31,13 @@ export class ErrorBrokerService {
} }
} }
showError(error: { message: string }) { showError(error: { message: string } | any) {
console.error(error); console.error(error);
if (this.errorCb) { if (this.errorCb) {
if (!error.message) { if (!error.message) {
this.errorCb({message: error}); this.errorCb({ message: error });
} else { } else {
this.errorCb({...error}); this.errorCb({ ...error });
} }
} }
} }

@ -2,10 +2,10 @@ import {Inject, Injectable} from "@angular/core";
import {File} from "../../../api/models/File"; import {File} from "../../../api/models/File";
import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser"; import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser";
import {SortKey} from "../../models/SortKey"; import {SortKey} from "../../models/SortKey";
import {GenericFilter} from "../../models/GenericFilter";
import {MediarepoApi} from "../../../api/Api"; import {MediarepoApi} from "../../../api/Api";
import {mapMany, mapNew} from "../../../api/models/adaptors"; import {mapMany, mapNew} from "../../../api/models/adaptors";
import {FileMetadata} from "../../../api/api-types/files"; import {FileMetadata} from "../../../api/api-types/files";
import {SearchFilters} from "../../../api/models/SearchFilters";
@Injectable({ @Injectable({
@ -22,17 +22,21 @@ export class FileService {
return MediarepoApi.getAllFiles().then(mapMany(mapNew(File))); return MediarepoApi.getAllFiles().then(mapMany(mapNew(File)));
} }
public async findFiles(filters: GenericFilter[], sortBy: SortKey[]): Promise<File[]> { public async findFiles(filters: SearchFilters, sortBy: SortKey[]): Promise<File[]> {
let backendFilters = filters.map(f => f.toBackendType()); return MediarepoApi.findFiles(
return MediarepoApi.findFiles({filters: backendFilters, sortBy: sortBy.map(k => k.toBackendType())}).then(mapMany(mapNew(File))); {
filters: filters.getFilters(),
sortBy: sortBy.map(k => k.toBackendType())
})
.then(mapMany(mapNew(File)));
} }
public async getFileMetadata(id: number): Promise<FileMetadata> { public async getFileMetadata(id: number): Promise<FileMetadata> {
return MediarepoApi.getFileMetadata({id}); return MediarepoApi.getFileMetadata({ id });
} }
public async updateFileName(id: number, name: string): Promise<FileMetadata> { public async updateFileName(id: number, name: string): Promise<FileMetadata> {
return MediarepoApi.updateFileName({id, name}); return MediarepoApi.updateFileName({ id, name });
} }
/** /**
@ -64,7 +68,7 @@ export class FileService {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async saveFile(file: File, targetPath: string) { public async saveFile(file: File, targetPath: string) {
await MediarepoApi.saveFileLocally({id: file.id, path: targetPath}); await MediarepoApi.saveFileLocally({ id: file.id, path: targetPath });
} }
/** /**
@ -73,7 +77,7 @@ export class FileService {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
public async deleteThumbnails(file: File) { public async deleteThumbnails(file: File) {
await MediarepoApi.deleteThumbnails({id: file.id}); await MediarepoApi.deleteThumbnails({ id: file.id });
} }
/** /**
@ -82,8 +86,9 @@ export class FileService {
* @returns {Promise<SafeResourceUrl>} * @returns {Promise<SafeResourceUrl>}
*/ */
public async readFile(file: File): Promise<SafeResourceUrl> { public async readFile(file: File): Promise<SafeResourceUrl> {
const data = await MediarepoApi.readFile({mimeType: file.mimeType, hash: file.cd}); const data = await MediarepoApi.readFile(
const blob = new Blob([new Uint8Array(data)], {type: file.mimeType}); { mimeType: file.mimeType, hash: file.cd });
const blob = new Blob([new Uint8Array(data)], { type: file.mimeType });
const url = URL?.createObjectURL(blob); const url = URL?.createObjectURL(blob);
return this.sanitizer.bypassSecurityTrustResourceUrl(url); return this.sanitizer.bypassSecurityTrustResourceUrl(url);
} }

@ -1,16 +1,16 @@
import { TestBed } from '@angular/core/testing'; import {TestBed} from "@angular/core/testing";
import { JobService } from './job.service'; import {JobService} from "./job.service";
describe('JobService', () => { describe("JobService", () => {
let service: JobService; let service: JobService;
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({}); TestBed.configureTestingModule({});
service = TestBed.inject(JobService); service = TestBed.inject(JobService);
}); });
it('should be created', () => { it("should be created", () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
}); });

@ -5,7 +5,7 @@ import {Injectable} from "@angular/core";
}) })
export class SchedulingService { export class SchedulingService {
private workQueue: { [key: string]: {id: number, cancelled: boolean, cb: Function}[] } = {} private workQueue: { [key: string]: { id: number, cancelled: boolean, cb: Function }[] } = {};
private lastWorkId = 0; private lastWorkId = 0;
constructor() { constructor() {
@ -17,7 +17,7 @@ export class SchedulingService {
setTimeout(() => this.startWork(key), 0); // start in the next tick setTimeout(() => this.startWork(key), 0); // start in the next tick
} }
const id = this.lastWorkId++; const id = this.lastWorkId++;
this.workQueue[key].push({id, cb, cancelled: false}); this.workQueue[key].push({ id, cb, cancelled: false });
return id; return id;
} }
@ -28,6 +28,12 @@ export class SchedulingService {
} }
} }
public async delay(time: number) {
return new Promise((res) => {
setTimeout(res, time);
});
}
private async startWork(key: string) { private async startWork(key: string) {
while (true) { while (true) {
if (this.workQueue[key]?.length > 0) { if (this.workQueue[key]?.length > 0) {
@ -47,10 +53,4 @@ export class SchedulingService {
await this.delay(1); await this.delay(1);
} }
} }
public async delay(time: number) {
return new Promise((res) => {
setTimeout(res, time);
});
}
} }

@ -1,5 +1,5 @@
@use 'sass:map'; @use 'sass:map';
@use "~@angular/material" as mat; @use "@angular/material" as mat;
@use 'src/app/app.component-theme' as app; @use 'src/app/app.component-theme' as app;
@use 'app/components/shared/file/file-card/file-card.component-theme' as file-card; @use 'app/components/shared/file/file-card/file-card.component-theme' as file-card;
@use 'app/components/shared/sidebar/file-search/file-search.component-theme' as file-search; @use 'app/components/shared/sidebar/file-search/file-search.component-theme' as file-search;
@ -7,15 +7,15 @@
@include mat.core(); @include mat.core();
$theme: mat.define-dark-theme(( $theme: mat.define-dark-theme((
color: ( color: (
primary: mat.define-palette(mat.$purple-palette, 700, 500, 800), primary: mat.define-palette(mat.$purple-palette, 700, 500, 800),
accent: mat.define-palette(mat.$green-palette, A200, A100, A400), accent: mat.define-palette(mat.$green-palette, A200, A100, A400),
warn: mat.define-palette(mat.$red-palette), warn: mat.define-palette(mat.$red-palette),
background: mat.define-palette(mat.$blue-grey-palette) background: mat.define-palette(mat.$blue-grey-palette)
), ),
typography: mat.define-typography-config( typography: mat.define-typography-config(
$font-family: 'Noto Sans', $font-family: 'Noto Sans',
) )
)); ));
$color-config: mat.get-color-config($theme); $color-config: mat.get-color-config($theme);

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save